Repository: freerange/mocha (Ruby)

Issue:
Sequencing failure messages are unclear
Given this example:

``` ruby
require "test/unit"
require "mocha"

class FooTest < Test::Unit::TestCase
  def test_foo
    sequence = sequence(:task_order)
    task_one = mock("task_one")
    task_two = mock("task_two")

    task_one.expects(:execute).in_sequence(sequence)
    task_two.expects(:execute).in_sequence(sequence)

    task_two.execute
    task_one.execute
  end
end
```

The failure message is unclear:

```
  1) Failure:
test_foo(FooTest) [example_spec.rb:13]:
unexpected invocation: #<Mock:task_two>.execute()
unsatisfied expectations:
- expected exactly once, not yet invoked: #<Mock:task_one>.execute(any_parameters); in sequence :task_order
- expected exactly once, not yet invoked: #<Mock:task_two>.execute(any_parameters); in sequence :task_order
```

I'd propose something like this:

```
  1) Failure:
test_foo(FooTest) [example_spec.rb:13]:
invocation: #<Mock:task_two>.execute() received out of order
```



Discussion:
I think that given there can be many sequences and many objects and methods involved in each, it may be quite hard to come up with a more meaningful error message, but I agree it is desirable. I'll look into it. Thanks for the feedback.

See also:
* https://gist.github.com/lukeredpath/1704783
* https://github.com/rspec/rspec-mocks/blob/main/features/setting_constraints/message_order.feature

[Released in v2.2.0](https://github.com/freerange/mocha/blob/main/RELEASE.md#220)



Patch:
diff --git a/lib/mocha/expectation.rb b/lib/mocha/expectation.rb
index 240747c76..5e6f95d4f 100644
--- a/lib/mocha/expectation.rb
+++ b/lib/mocha/expectation.rb
@@ -632,14 +632,20 @@ def in_correct_order?
       @ordering_constraints.all?(&:allows_invocation_now?)
     end
 
+    # @private
+    def ordering_constraints_not_allowing_invocation_now
+      @ordering_constraints.reject(&:allows_invocation_now?)
+    end
+
     # @private
     def matches_method?(method_name)
       @method_matcher.match?(method_name)
     end
 
     # @private
-    def match?(invocation)
-      @method_matcher.match?(invocation.method_name) && @parameters_matcher.match?(invocation.arguments) && @block_matcher.match?(invocation.block) && in_correct_order?
+    def match?(invocation, ignoring_order: false)
+      order_independent_match = @method_matcher.match?(invocation.method_name) && @parameters_matcher.match?(invocation.arguments) && @block_matcher.match?(invocation.block)
+      ignoring_order ? order_independent_match : order_independent_match && in_correct_order?
     end
 
     # @private
diff --git a/lib/mocha/expectation_list.rb b/lib/mocha/expectation_list.rb
index f5366ec17..74c720da4 100644
--- a/lib/mocha/expectation_list.rb
+++ b/lib/mocha/expectation_list.rb
@@ -17,7 +17,11 @@ def matches_method?(method_name)
       @expectations.any? { |expectation| expectation.matches_method?(method_name) }
     end
 
-    def match(invocation)
+    def match(invocation, ignoring_order: false)
+      matching_expectations(invocation, ignoring_order: ignoring_order).first
+    end
+
+    def match_but_out_of_order(invocation)
       matching_expectations(invocation).first
     end
 
@@ -51,8 +55,8 @@ def +(other)
 
     private
 
-    def matching_expectations(invocation)
-      @expectations.select { |e| e.match?(invocation) }
+    def matching_expectations(invocation, ignoring_order: false)
+      @expectations.select { |e| e.match?(invocation, ignoring_order: ignoring_order) }
     end
   end
 end
diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb
index 397d602d6..6b2b9737d 100644
--- a/lib/mocha/mock.rb
+++ b/lib/mocha/mock.rb
@@ -323,7 +323,7 @@ def handle_method_call(symbol, arguments, block)
       invocation = Invocation.new(self, symbol, arguments, block)
       if (matching_expectation_allowing_invocation = all_expectations.match_allowing_invocation(invocation))
         matching_expectation_allowing_invocation.invoke(invocation)
-      elsif (matching_expectation = all_expectations.match(invocation)) || (!matching_expectation && !@everything_stubbed)
+      elsif (matching_expectation = all_expectations.match(invocation, ignoring_order: true)) || (!matching_expectation && !@everything_stubbed)
         raise_unexpected_invocation_error(invocation, matching_expectation)
       end
     end
@@ -373,7 +373,11 @@ def raise_unexpected_invocation_error(invocation, matching_expectation)
       if @unexpected_invocation.nil?
         @unexpected_invocation = invocation
         matching_expectation.invoke(invocation) if matching_expectation
-        message = "#{@unexpected_invocation.call_description}\n#{@mockery.mocha_inspect}"
+        call_description = @unexpected_invocation.call_description
+        if matching_expectation && !matching_expectation.in_correct_order?
+          call_description += ' invoked out of order'
+        end
+        message = "#{call_description}\n#{@mockery.mocha_inspect}"
       else
         message = @unexpected_invocation.short_call_description
       end


Test patch:
diff --git a/test/acceptance/sequence_block_test.rb b/test/acceptance/sequence_block_test.rb
index 468191424..86513cc9f 100644
--- a/test/acceptance/sequence_block_test.rb
+++ b/test/acceptance/sequence_block_test.rb
@@ -24,6 +24,7 @@ def test_should_constrain_invocations_to_occur_in_expected_order
       mock.first
     end
     assert_failed(test_result)
+    assert_match 'second() invoked out of order', test_result.failures.first.message
   end
 
   def test_should_allow_invocations_in_sequence
@@ -87,6 +88,7 @@ def test_should_constrain_invocations_to_occur_in_expected_order_even_if_expecte
       partial_mock_one.first
     end
     assert_failed(test_result)
+    assert_match 'second() invoked out of order', test_result.failures.first.message
   end
 
   def test_should_allow_invocations_in_sequence_even_if_expected_on_partial_mocks
diff --git a/test/acceptance/sequence_test.rb b/test/acceptance/sequence_test.rb
index 85ec8cbc7..52bcd6145 100644
--- a/test/acceptance/sequence_test.rb
+++ b/test/acceptance/sequence_test.rb
@@ -23,6 +23,7 @@ def test_should_constrain_invocations_to_occur_in_expected_order
       mock.first
     end
     assert_failed(test_result)
+    assert_match 'second() invoked out of order', test_result.failures.first.message
   end
 
   def test_should_allow_invocations_in_sequence
@@ -82,6 +83,7 @@ def test_should_constrain_invocations_to_occur_in_expected_order_even_if_expecte
       partial_mock_one.first
     end
     assert_failed(test_result)
+    assert_match 'second() invoked out of order', test_result.failures.first.message
   end
 
   def test_should_allow_invocations_in_sequence_even_if_expected_on_partial_mocks