RSpec Association Matchers
1 2 describe Product, 'with Group' do 3 4 it 'should belong to group' do 5 Product.should belong_to(:product_group) 6 end 7 8 end 9 10 describe ProductGroup, 'with Product' do 11 12 it 'should have many products depending (on group)' do 13 ProductGroup.should have_many(:products).depending 14 end 15 16 end
Here is the code:
1 2 module AssociationMatchers 3 4 class AssociationReflection 5 6 def initialize(type, name) 7 @messages = { 8 :missing_association => 9 '%s is not associated with %s.', 10 :wrong_type => 11 "%s %s %s./nExpected: %s", 12 :wrong_options => 13 "Options are incorrect.\nExpected: %s Got: %s", 14 :missing_column => 15 "Missing foreign key.\nExpected: %s" 16 } 17 @name = name 18 @expected_type = type 19 @expected_options = {} 20 end 21 22 def matches?(target) 23 Class === target or 24 raise ArgumentError, 'class expected' 25 26 @target = target 27 28 unless @assoc = target.reflect_on_association(@name) 29 @failure = :missing_association 30 return false 31 end 32 33 unless @assoc.macro.eql?(@expected_type) 34 @failure = :wrong_type 35 return false 36 end 37 38 if @expected_options.any? { |o| @assoc.options[o.first] != o.last } 39 @failure = :wrong_options 40 return false 41 end 42 43 @column ||= @assoc.primary_key_name || @assoc.klass.name.foreign_key 44 45 @failure = case @assoc.macro.to_s 46 when 'belongs_to' 47 if @target.column_names.include?(@column.to_s) then nil 48 else 49 :missing_column 50 end 51 when /(?:has_many|has_one)/ 52 if @assoc.options[:through] then nil 53 elsif @assoc.klass.column_names.include?(@column.to_s) then nil 54 else 55 :missing_column 56 end 57 end 58 59 return @failure.nil? 60 end 61 62 def failure_message 63 case @failure 64 when :missing_association 65 @messages[@failure] % [@target.name, @name] 66 when :wrong_type 67 @messages[@failure] % [ 68 @target.name, 69 @assoc.macro, 70 @name, 71 @expected_type 72 ] 73 when :wrong_options 74 @messages[@failure] % [ 75 @expected_options.inspect, 76 @assoc.options.inspect 77 ] 78 when :missing_column 79 @messages[@failure] % @column 80 end 81 end 82 def negative_failure_message 83 end 84 85 ### Generic Options 86 87 def of(class_name) 88 class_name = class_name.name if Class === class_name 89 @expected_options[:class_name] = class_name 90 self 91 end 92 def for(foreign_key) 93 @column = foreign_key 94 self 95 end 96 def due_to(conditions) 97 @expected_options[:conditions] = conditions 98 self 99 end 100 def ordered_by(statement) 101 @expected_options[:order] = statement 102 self 103 end 104 def including(*models) 105 @expected_options[:include] = (models.length == 1)? models.first: models 106 self 107 end 108 109 end 110 class BelongsToReflection < AssociationReflection 111 112 def initialize(name) 113 super :belongs_to, name 114 end 115 116 def counted(column) 117 @expected_options[:counter_cache] = column 118 self 119 end 120 def polymorphic(true_or_false = true) 121 @expected_options[:polymorphic] = true_or_false 122 self 123 end 124 125 end 126 127 class HasOneReflection < AssociationReflection 128 129 def initialize(name) 130 super :has_one, name 131 end 132 133 def as(interface_name) 134 @expected_options[:as] = interface_name 135 self 136 end 137 def depending(dependency = true) 138 @expected_options[:dependent] = dependency 139 self 140 end 141 def extended_by(mod) 142 @expected_options[:extend] = mod 143 self 144 end 145 146 end 147 class HasManyReflection < AssociationReflection 148 149 def initialize(name) 150 super :has_many, name 151 end 152 153 def as(interface_name) 154 @expected_options[:as] = interface_name 155 self 156 end 157 def depending(dependency = :destroy) 158 @expected_options[:dependent] = dependency 159 self 160 end 161 162 end 163 class HasAndBelongsToManyReflection < AssociationReflection 164 165 def initialize(name) 166 super :has_and_belongs_to_many, name 167 end 168 169 end 170 171 def belong_to(model) 172 BelongsToReflection.new model 173 end 174 def have_one(model) 175 HasOneReflection.new model 176 end 177 def have_many(models) 178 HasManyReflection.new models 179 end 180 def have_and_belong_to_many(models) 181 HasAndBelongsToManyReflection.new models 182 end 183 alias_method :habtm, :have_and_belong_to_many 184 185 end
It checks:
* association exists
* association macro
* foreign key exists (except for habtm)
* options match (only a subset is supported)
Setup:
* put the code in RAILS_ROOT + "/lib/association_matchers.rb"
* put "config.include AssociationMatchers # lib/association_matchers.rb" in your spec_helper.rb configure block
* refactor your model specs...