warp 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +35 -8
  5. data/Appraisals +67 -0
  6. data/Gemfile +4 -5
  7. data/README.md +57 -8
  8. data/Rakefile +4 -1
  9. data/gemfiles/rails_3.2_rspec_2.14.gemfile +22 -0
  10. data/gemfiles/rails_3.2_rspec_2.99.0.beta1.gemfile +22 -0
  11. data/gemfiles/rails_3.2_rspec_3.0.0.beta1.gemfile +23 -0
  12. data/gemfiles/rails_3.2_rspec_master.gemfile +23 -0
  13. data/gemfiles/rails_4.0_rspec_2.14.gemfile +22 -0
  14. data/gemfiles/rails_4.0_rspec_2.99.0.beta1.gemfile +22 -0
  15. data/gemfiles/rails_4.0_rspec_3.0.0.beta1.gemfile +23 -0
  16. data/gemfiles/rails_4.0_rspec_master.gemfile +23 -0
  17. data/gemfiles/rails_4.1.0.beta1_rspec_2.14.gemfile +23 -0
  18. data/gemfiles/rails_4.1.0.beta1_rspec_2.99.0.beta1.gemfile +23 -0
  19. data/gemfiles/rails_4.1.0.beta1_rspec_3.0.0.beta1.gemfile +24 -0
  20. data/gemfiles/rails_4.1.0.beta1_rspec_master.gemfile +24 -0
  21. data/lib/warp/controller_matchers/assign_matcher.rb +1 -3
  22. data/lib/warp/controller_matchers/set_flash_matcher.rb +19 -14
  23. data/lib/warp/matcher.rb +30 -0
  24. data/lib/warp/model_matchers/association_matcher.rb +56 -0
  25. data/lib/warp/model_matchers/attribute_matcher.rb +32 -0
  26. data/lib/warp/model_matchers/error_matcher.rb +12 -0
  27. data/lib/warp/model_matchers/matcher.rb +15 -0
  28. data/lib/warp/model_matchers/validation_matcher.rb +12 -0
  29. data/lib/warp/model_matchers.rb +10 -0
  30. data/lib/warp/version.rb +13 -1
  31. data/lib/warp.rb +4 -1
  32. data/spec/spec_helper.rb +15 -6
  33. data/spec/support/failure_message_helpers.rb +23 -0
  34. data/spec/support/match_helpers.rb +24 -12
  35. data/spec/support/model_helpers.rb +25 -0
  36. data/spec/support/with_contexts_helpers.rb +6 -0
  37. data/spec/warp/controller_matchers/assign_matcher_spec.rb +35 -33
  38. data/spec/warp/controller_matchers/set_flash_matcher_spec.rb +4 -12
  39. data/spec/warp/model_helpers/association_matcher_spec.rb +102 -0
  40. data/spec/warp/model_helpers/attribute_matcher_spec.rb +43 -0
  41. data/warp.gemspec +2 -4
  42. metadata +31 -33
  43. data/gemfiles/rails-3.2 +0 -17
  44. data/gemfiles/rails-4.0 +0 -17
  45. data/gemfiles/rails-4.1 +0 -18
@@ -0,0 +1,30 @@
1
+ module Warp
2
+ class Matcher
3
+ # Composable matchers are new in RSpec 3.
4
+ # Define basic helpers in their absence.
5
+ if defined?(RSpec::Matchers::Composable)
6
+ include RSpec::Matchers::Composable
7
+ else
8
+ def description_of(object)
9
+ object.inspect
10
+ end
11
+
12
+ def values_match?(expected, actual)
13
+ expected === actual || actual == expected
14
+ end
15
+ end
16
+
17
+ # RSpec 2 and 3 have different methods
18
+ # that they call on matcher to get the
19
+ # failure messages.
20
+ if RSpec::Version::STRING[0] == "2"
21
+ def failure_message_for_should
22
+ failure_message
23
+ end
24
+
25
+ def failure_message_for_should_not
26
+ failure_message_when_negated
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,56 @@
1
+ module Warp
2
+ module ModelMatchers
3
+ class AssociationMatcher < Warp::ModelMatchers::Matcher
4
+ attr_reader :macro, :key
5
+ attr_reader :failure_message, :failure_message_when_negated, :description
6
+
7
+ def initialize(macro, key)
8
+ @macro = macro
9
+ @key = key
10
+ end
11
+
12
+ def matches?(model_or_instance)
13
+ if association = model(model_or_instance).reflect_on_association(key)
14
+ actual_macro = association.macro
15
+ @failure_message = "expected to have association #{macro} :#{key}, but had #{actual_macro} :#{key}"
16
+ @failure_message_when_negated = "expected to not have association #{macro} :#{key}"
17
+ actual_macro == macro
18
+ else
19
+ @failure_message = "expected to have association #{macro} :#{key}"
20
+ @failure_message_when_negated = "expected to not have association #{macro} :#{key}"
21
+ false
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def model(model_or_instance)
28
+ if model_or_instance.is_a? Class
29
+ model_or_instance
30
+ else
31
+ model_or_instance.class
32
+ end
33
+ end
34
+ end
35
+
36
+ def have_many(key)
37
+ AssociationMatcher.new(:has_many, key)
38
+ end
39
+
40
+ def have_one(key)
41
+ AssociationMatcher.new(:has_one, key)
42
+ end
43
+
44
+ def belong_to(key)
45
+ AssociationMatcher.new(:belongs_to, key)
46
+ end
47
+
48
+ def have_and_belong_to_many(key)
49
+ if ActiveRecord::VERSION::STRING[0] == "4" && ActiveRecord::VERSION::STRING[3] != "0"
50
+ raise NotImplementedError, "In Rail 4.1+ the has_and_belongs_to_many helper produces a has_many :through association."
51
+ else
52
+ AssociationMatcher.new(:has_and_belongs_to_many, key)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,32 @@
1
+ module Warp
2
+ module ModelMatchers
3
+ class AttributeMatcher < Warp::ModelMatchers::Matcher
4
+ attr_reader :attr_name
5
+ attr_reader :failure_message, :failure_message_when_negated, :description
6
+
7
+ def initialize(attr_name)
8
+ @attr_name = attr_name.to_sym
9
+ end
10
+
11
+ def matches?(model_or_instance)
12
+ if attributes(model_or_instance).any? {|actual| values_match?(attr_name, actual) }
13
+ @failure_message = "expected to have attribute #{description_of(attr_name)}"
14
+ @failure_message_when_negated = "expected to not have attribute #{description_of(attr_name)}"
15
+ true
16
+ else
17
+ @failure_message = "expected to have attribute #{description_of(attr_name)}"
18
+ @failure_message_when_negated = "expected to not have attribute #{description_of(attr_name)}"
19
+ false
20
+ end
21
+ end
22
+
23
+ def attributes(model_or_instance)
24
+ model(model_or_instance).column_names.map(&:to_sym)
25
+ end
26
+ end
27
+
28
+ def have_attribute(attr_name)
29
+ AttributeMatcher.new(attr_name)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ module Warp
2
+ module ModelMatchers
3
+ class ErrorMatcher < Warp::Matcher
4
+
5
+
6
+ end
7
+
8
+ def have_error
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ module Warp
2
+ module ModelMatchers
3
+ class Matcher < Warp::Matcher
4
+ private
5
+
6
+ def model(model_or_instance)
7
+ if model_or_instance.is_a? Class
8
+ model_or_instance
9
+ else
10
+ model_or_instance.class
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module Warp
2
+ module ModelMatchers
3
+ class ValidationMatcher < Warp::Matcher
4
+
5
+
6
+ end
7
+
8
+ def validate
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ require "warp/model_matchers/matcher"
2
+
3
+ require "warp/model_matchers/association_matcher"
4
+ require "warp/model_matchers/attribute_matcher"
5
+ # require "warp/model_matchers/error_matcher"
6
+ # require "warp/model_matchers/validation_matcher"
7
+
8
+ RSpec.configure do |config|
9
+ config.include Warp::ModelMatchers
10
+ end
data/lib/warp/version.rb CHANGED
@@ -1,3 +1,15 @@
1
1
  module Warp
2
- VERSION = "1.0.1"
2
+ module VERSION
3
+ MAJOR = 1
4
+ MINOR = 1
5
+ PATCH = 0
6
+
7
+ BETA = nil
8
+
9
+ def self.to_s
10
+ version_str = [MAJOR, MINOR, PATCH].map(&:to_s).join(".")
11
+ version_str << ".beta#{BETA}" if BETA
12
+ version_str
13
+ end
14
+ end
3
15
  end
data/lib/warp.rb CHANGED
@@ -2,4 +2,7 @@ require "warp/version"
2
2
 
3
3
  require "rspec"
4
4
 
5
- require "warp/controller_matchers"
5
+ require "warp/matcher"
6
+
7
+ require "warp/controller_matchers"
8
+ require "warp/model_matchers"
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,15 @@
1
- require 'bundler'
1
+ require "bundler"
2
+
3
+ require "simplecov"
4
+
5
+ if ENV["TRAVIS_CI"]
6
+ require "coveralls"
7
+ SimpleCov.formatter = Coveralls::SimpleCov::Formatter
8
+ end
9
+
10
+ SimpleCov.start do
11
+ add_filter "/spec/"
12
+ end
2
13
 
3
14
  if ENV["TRAVIS_CI"]
4
15
  Bundler.require
@@ -6,16 +17,14 @@ else
6
17
  Bundler.require(:default, :tools)
7
18
  end
8
19
 
9
- require "active_support/all"
10
- require "action_controller"
11
-
12
- require "warp"
13
-
14
20
  Dir[File.expand_path("../support/**/*.rb", __FILE__)].each {|f| require f }
15
21
 
16
22
  RSpec.configure do |config|
17
23
  config.order = 'random'
18
24
 
19
25
  config.extend ControllerHelpers
26
+ config.extend FailureMessageHelpers
27
+ config.extend ModelHelpers
20
28
  config.extend WithContextsHelpers
29
+ config.include MatchHelpers
21
30
  end
@@ -0,0 +1,23 @@
1
+ module FailureMessageHelpers
2
+ if RSpec::Version::STRING[0] == "3"
3
+ FAILURE_MESSAGE_METHOD = :failure_message
4
+ FAILURE_MESSAGE_WHEN_NEGATED_METHOD = :failure_message_when_negated
5
+ else
6
+ FAILURE_MESSAGE_METHOD = :failure_message_for_should
7
+ FAILURE_MESSAGE_WHEN_NEGATED_METHOD = :failure_message_for_should_not
8
+ end
9
+
10
+ def describe_failure_message(&blk)
11
+ describe "##{FAILURE_MESSAGE_METHOD}" do
12
+ subject { super().send(FAILURE_MESSAGE_METHOD) }
13
+ instance_eval(&blk)
14
+ end
15
+ end
16
+
17
+ def describe_failure_message_when_negated(&blk)
18
+ describe "##{FAILURE_MESSAGE_WHEN_NEGATED_METHOD}" do
19
+ subject { super().send(FAILURE_MESSAGE_WHEN_NEGATED_METHOD) }
20
+ instance_eval(&blk)
21
+ end
22
+ end
23
+ end
@@ -1,18 +1,30 @@
1
- RSpec::Matchers.define :match do |value|
2
- match do |matcher|
3
- @matcher = matcher
4
- matcher.matches?(value)
5
- end
1
+ module MatchHelpers
2
+ class MatchMatcher
3
+ attr_reader :value, :matcher
6
4
 
7
- description do
8
- "match #{value}"
9
- end
5
+ def initialize(value)
6
+ @value = value
7
+ end
8
+
9
+ def matches?(matcher)
10
+ @matcher = matcher
11
+ matcher.matches?(value)
12
+ end
13
+
14
+ def description
15
+ "match #{value}"
16
+ end
17
+
18
+ def failure_message
19
+ "expect #{matcher} to match #{value} but did not"
20
+ end
10
21
 
11
- failure_message do
12
- "expect #{@matcher} to match #{value} but did not"
22
+ def failure_message_when_negated
23
+ "expect #{matcher} to not match #{value} but did"
24
+ end
13
25
  end
14
26
 
15
- failure_message_when_negated do
16
- "expect #{@matcher} to not match #{value} but did"
27
+ def match(value)
28
+ MatchMatcher.new(value)
17
29
  end
18
30
  end
@@ -0,0 +1,25 @@
1
+ module ModelHelpers
2
+ def build_model(&blk)
3
+ let(:model) do
4
+ Class.new(ActiveRecord::Base) do
5
+ def self.name
6
+ "TestModel"
7
+ end
8
+
9
+ def self.primary_key
10
+ "id"
11
+ end
12
+
13
+ def self.columns
14
+ @columns ||= []
15
+ end
16
+
17
+ def self.column(name, sql_type = nil, default = nil, null = true)
18
+ columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
19
+ end
20
+
21
+ instance_eval(&blk)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -2,6 +2,8 @@ module WithContextsHelpers
2
2
  class WithContextsBuilder
3
3
  attr_accessor :contexts, :examples, :group
4
4
 
5
+ delegate :let, :before, :after, :subject, to: :group
6
+
5
7
  def initialize(group)
6
8
  @contexts = {}
7
9
  @group = group
@@ -11,6 +13,10 @@ module WithContextsHelpers
11
13
  self.contexts[name] = blk
12
14
  end
13
15
 
16
+ def describe(name, &blk)
17
+ self.contexts[name] = blk
18
+ end
19
+
14
20
  def behaviour(&blk)
15
21
  self.examples = blk
16
22
  end
@@ -35,9 +35,7 @@ describe Warp::ControllerMatchers::AssignMatcher do
35
35
 
36
36
  specify { expect(subject).to match(_controller) }
37
37
 
38
- describe "#failure_message_when_negated" do
39
- subject { super().failure_message_when_negated }
40
-
38
+ describe_failure_message_when_negated do
41
39
  specify { expect(subject).to eq "expected @assign to not be assigned" }
42
40
  end
43
41
  end
@@ -49,9 +47,7 @@ describe Warp::ControllerMatchers::AssignMatcher do
49
47
 
50
48
  specify { expect(subject).to_not match(_controller) }
51
49
 
52
- describe "#failure_message" do
53
- subject { super().failure_message }
54
-
50
+ describe_failure_message do
55
51
  specify { expect(subject).to eq "expected @assign to be assigned" }
56
52
  end
57
53
  end
@@ -75,9 +71,7 @@ describe Warp::ControllerMatchers::AssignMatcher do
75
71
  context "with the right value" do
76
72
  specify { expect(subject).to match(_controller) }
77
73
 
78
- describe "#failure_message_when_negated" do
79
- subject { super().failure_message_when_negated }
80
-
74
+ describe_failure_message_when_negated do
81
75
  specify { expect(subject).to eq "expected @assign to not be assigned with #{expected_assign_value.inspect}" }
82
76
  end
83
77
  end
@@ -87,9 +81,7 @@ describe Warp::ControllerMatchers::AssignMatcher do
87
81
 
88
82
  specify { expect(subject).to_not match(_controller) }
89
83
 
90
- describe "#failure_message" do
91
- subject { super().failure_message }
92
-
84
+ describe_failure_message do
93
85
  specify { expect(subject).to eq "expected @assign to be assigned with #{expected_assign_value.inspect} but was assigned with #{actual_assign_value.inspect}" }
94
86
  end
95
87
  end
@@ -114,9 +106,7 @@ describe Warp::ControllerMatchers::AssignMatcher do
114
106
  context "with the right class" do
115
107
  specify { expect(subject).to match(_controller) }
116
108
 
117
- describe "#failure_message_when_negated" do
118
- subject { super().failure_message_when_negated }
119
-
109
+ describe_failure_message_when_negated do
120
110
  specify { expect(subject).to eq "expected @assign to not be assigned with an instance of #{expected_assign_class.name}" }
121
111
  end
122
112
  end
@@ -126,9 +116,7 @@ describe Warp::ControllerMatchers::AssignMatcher do
126
116
 
127
117
  specify { expect(subject).to_not match(_controller) }
128
118
 
129
- describe "#failure_message" do
130
- subject { super().failure_message }
131
-
119
+ describe_failure_message do
132
120
  specify { expect(subject).to eq "expected @assign to be assigned with an instance of #{expected_assign_class.name} but was assigned with an instance of #{actual_assign_class.name}"
133
121
  }
134
122
  end
@@ -164,9 +152,7 @@ describe Warp::ControllerMatchers::AssignMatcher do
164
152
  context "with the right class" do
165
153
  specify { expect(subject).to match(_controller) }
166
154
 
167
- describe "#failure_message_when_negated" do
168
- subject { super().failure_message_when_negated }
169
-
155
+ describe_failure_message_when_negated do
170
156
  specify { expect(subject).to eq "expected @assign to not be assigned with a new instance of #{expected_assign_class.name}" }
171
157
  end
172
158
  end
@@ -178,9 +164,7 @@ describe Warp::ControllerMatchers::AssignMatcher do
178
164
 
179
165
  specify { expect(subject).to match(_controller) }
180
166
 
181
- describe "#failure_message_when_negated" do
182
- subject { super().failure_message_when_negated }
183
-
167
+ describe_failure_message_when_negated do
184
168
  specify { expect(subject).to eq "expected @assign to not be assigned with a new instance of #{expected_assign_class.name}" }
185
169
  end
186
170
  end
@@ -190,9 +174,7 @@ describe Warp::ControllerMatchers::AssignMatcher do
190
174
 
191
175
  specify { expect(subject).to_not match(_controller) }
192
176
 
193
- describe "#failure_message" do
194
- subject { super().failure_message }
195
-
177
+ describe_failure_message do
196
178
  specify { expect(subject).to eq "expected @assign to be assigned with a new instance of #{expected_assign_class.name} but was assigned with a new instance of #{actual_assign_class.name}"
197
179
  }
198
180
  end
@@ -205,9 +187,7 @@ describe Warp::ControllerMatchers::AssignMatcher do
205
187
  context "with the right class" do
206
188
  specify { expect(subject).to_not match(_controller) }
207
189
 
208
- describe "#failure_message" do
209
- subject { super().failure_message }
210
-
190
+ describe_failure_message do
211
191
  specify { expect(subject).to eq "expected @assign to be assigned with a new instance of #{expected_assign_class.name} but was assigned with a persisted instance of #{actual_assign_class.name}"
212
192
  }
213
193
  end
@@ -218,15 +198,37 @@ describe Warp::ControllerMatchers::AssignMatcher do
218
198
 
219
199
  specify { expect(subject).to_not match(_controller) }
220
200
 
221
- describe "#failure_message" do
222
- subject { super().failure_message }
223
-
201
+ describe_failure_message do
224
202
  specify { expect(subject).to eq "expected @assign to be assigned with a new instance of #{expected_assign_class.name} but was assigned with a persisted instance of #{actual_assign_class.name}"
225
203
  }
226
204
  end
227
205
  end
228
206
  end
229
207
  end
208
+
209
+ context "multiple assertions" do
210
+ with_contexts do
211
+ context "with .with and .with_a" do
212
+ let(:matcher) { super().with(Object.new).with_a(Class.new) }
213
+ end
214
+
215
+ context "with .with_a and .with_a_new" do
216
+ let(:matcher) { super().with_a(Class.new).with_a_new(Class.new) }
217
+ end
218
+
219
+ context "with .with_a_new and .with" do
220
+ let(:matcher) { super().with_a_new(Class.new).with(Object.new) }
221
+ end
222
+
223
+ context "with .with, .with_a, and .with_a_new" do
224
+ let(:matcher) { super().with(Object.new).with_a(Class.new).with_a_new(Class.new) }
225
+ end
226
+
227
+ behaviour do
228
+ specify { expect{ subject }.to raise_error("Only one of .with, .with_a, and .with_a_new can be used with the assigns matcher.") }
229
+ end
230
+ end
231
+ end
230
232
  end
231
233
  end
232
234
  end
@@ -39,9 +39,7 @@ describe Warp::ControllerMatchers::SetFlashMatcher do
39
39
 
40
40
  specify { expect(subject).to match(_controller) }
41
41
 
42
- describe "#failure_message_when_negated" do
43
- subject { super().failure_message_when_negated }
44
-
42
+ describe_failure_message_when_negated do
45
43
  specify { expect(subject).to eq "expected flash[:#{flash_key}] to not be set" }
46
44
  end
47
45
  end
@@ -49,9 +47,7 @@ describe Warp::ControllerMatchers::SetFlashMatcher do
49
47
  context "with no flash set" do
50
48
  specify { expect(subject).to_not match(_controller) }
51
49
 
52
- describe "#failure_message" do
53
- subject { super().failure_message }
54
-
50
+ describe_failure_message do
55
51
  specify { expect(subject).to eq "expected flash[:#{flash_key}] to be set" }
56
52
  end
57
53
  end
@@ -76,9 +72,7 @@ describe Warp::ControllerMatchers::SetFlashMatcher do
76
72
 
77
73
  specify { expect(subject).to match(_controller) }
78
74
 
79
- describe "#failure_message_when_negated" do
80
- subject { super().failure_message_when_negated }
81
-
75
+ describe_failure_message_when_negated do
82
76
  specify { expect(subject).to eq "expected flash[:#{flash_key}] to not be set to #{expected_flash_value.inspect}" }
83
77
  end
84
78
  end
@@ -88,9 +82,7 @@ describe Warp::ControllerMatchers::SetFlashMatcher do
88
82
 
89
83
  specify { expect(subject).to_not match(_controller) }
90
84
 
91
- describe "#failure_message" do
92
- subject { super().failure_message }
93
-
85
+ describe_failure_message do
94
86
  specify { expect(subject).to eq "expected flash[:#{flash_key}] to be set to #{expected_flash_value.inspect}" }
95
87
  end
96
88
  end
@@ -0,0 +1,102 @@
1
+ require "spec_helper"
2
+
3
+ describe Warp::ModelMatchers::AssociationMatcher do
4
+ build_model do
5
+ belongs_to :foo
6
+ has_many :bars
7
+ has_one :baz
8
+ has_and_belongs_to_many :qux
9
+ end
10
+
11
+ with_contexts do
12
+ context "with model" do
13
+ let(:model_or_instance) { model }
14
+ end
15
+
16
+ context "with model instance" do
17
+ let(:model_or_instance) { model.new }
18
+ end
19
+
20
+ behaviour do
21
+ if ActiveRecord::VERSION::STRING[0] == "4" && ActiveRecord::VERSION::STRING[3] != "0"
22
+ specify { expect{ have_and_belong_to_many(:foo) }.to raise_error(NotImplementedError) }
23
+ end
24
+
25
+ with_contexts do
26
+ let(:no_association_key) { :foobar }
27
+
28
+ describe "#have_many" do
29
+ let(:matcher) { have_many(key) }
30
+
31
+ let(:matcher_macro) { :has_many }
32
+ let(:association_key) { :bars }
33
+ let(:wrong_association_key) { :foo }
34
+ end
35
+
36
+ describe "#have_one" do
37
+ let(:matcher) { have_one(key) }
38
+
39
+ let(:matcher_macro) { :has_one }
40
+ let(:association_key) { :baz }
41
+ let(:wrong_association_key) { :foo }
42
+ end
43
+
44
+ describe "#belong_to" do
45
+ let(:matcher) { belong_to(key) }
46
+
47
+ let(:matcher_macro) { :belongs_to }
48
+ let(:association_key) { :foo }
49
+ let(:wrong_association_key) { :bars }
50
+ end
51
+
52
+
53
+ unless ActiveRecord::VERSION::STRING[0] == "4" && ActiveRecord::VERSION::STRING[3] != "0"
54
+ describe "#have_and_belong_to_many" do
55
+ let(:matcher) { have_and_belong_to_many(key) }
56
+
57
+ let(:matcher_macro) { :has_and_belongs_to_many }
58
+ let(:association_key) { :qux }
59
+ let(:wrong_association_key) { :foo }
60
+ end
61
+ end
62
+
63
+ behaviour do
64
+ subject { matcher.tap {|m| m.matches?(model_or_instance) } }
65
+
66
+ context "when an association exists" do
67
+ context "and the association macro matches" do
68
+ let(:key) { association_key }
69
+
70
+ specify { expect(subject).to match(model_or_instance) }
71
+
72
+ describe_failure_message_when_negated do
73
+ specify { expect(subject).to eq "expected to not have association #{matcher_macro} :#{key}" }
74
+ end
75
+ end
76
+
77
+ context "and the association macro doesn't match" do
78
+ let(:key) { wrong_association_key }
79
+ let(:actual_macro) { model.reflect_on_association(key).macro }
80
+
81
+ specify { expect(subject).to_not match(model_or_instance) }
82
+
83
+ describe_failure_message do
84
+ specify { expect(subject).to eq "expected to have association #{matcher_macro} :#{key}, but had #{actual_macro} :#{key}" }
85
+ end
86
+ end
87
+ end
88
+
89
+ context "when an association doesn't exists" do
90
+ let(:key) { no_association_key }
91
+
92
+ specify { expect(subject).to_not match(model_or_instance) }
93
+
94
+ describe_failure_message do
95
+ specify { expect(subject).to eq "expected to have association #{matcher_macro} :#{key}" }
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end