-
-
Notifications
You must be signed in to change notification settings - Fork 159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
.yields()
failing when the caller does not provide a block breaks legitimate tests
#436
Comments
yields
failing when the caller does not provide a block breaks legitimate tests.yields()
failing when the caller does not provide a block breaks legitimate tests
Thanks for reporting, @yemartin. If you're able to make a few changes to either the test or the application code, I think you work with the new behavior which I still think is more correct/better than the old behavior. Here'a a few observations/suggestions I have: The error you're getting is due to the last line in this method not giving a block to def self.call
results = []
expensive_method do |yielded|
results << yielded
end
results << expensive_method
end Without much context, it seems to me you're calling def self.call
results = []
results << expensive_method do |yielded|
results << yielded
end
end This would stop your test failing as well, because Alternatively, you could change your test from: def test_both_returns_and_yields
MyService
.stubs(:expensive_method)
.returns("returned")
.yields("yielded")
call_result = MyService.call
assert_includes call_result, "returned"
assert_includes call_result, "yielded"
end to: def test_both_returns_and_yields
MyService.stubs(:expensive_method).returns("returned")
MyService.stubs(:expensive_method).yields("yielded").returns("returned")
call_result = MyService.call
assert_includes call_result, "returned"
assert_includes call_result, "yielded"
end This sets up I've tried both of those approaches and they get the tests passing. Does any of those approaches work for you? |
Hi @nitishr, and thank you for the ideas. I won't pretend to understand why you think the new behavior is more correct, this goes way above my head... But my use case is this: multiple calls, in a potentially unpredictable sequence, to a method that both yields and returns, and that needs stubbing.
Changing application code is unfortunately not an option, and anyway, it is very bad practice to modify production code in order to satisfy the idiosyncrasies of testing. I am, however, totally open to changing the test code.
The problem with the test code change is that it requires knowing the exact number, type (with or without block), and order of all invocations. That is easy to do in this simplistic example, but my actual broken test is a high level integration test. The method is called many times during the test, so hardcoding that exact sequence of invocations as stubs would be quite tedious. Worse, it would also be a terrible practice, as it means totally coupling a feature test to the exact implementation, making the test extremely brittle since any refactoring would likely break it. And I am not even sure it is technically possible to hardcode the sequence of invocations: test randomness may cause the exact sequence to change from one test run to the next. I am open to other ideas, but so far, by the looks of things, Mocha 1.10 has broken the use case I describe. |
Thank you for reporting the issue and for giving such clear examples. I'm sorry this has caused you problems. In hindsight, although the old behaviour was undocumented, I probably should have added a deprecation warning before changing it - sorry! I think the new behaviour does makes sense, but I think we probably need to do a better job of explaining why. I'm going to investigate how feasible it would be to reinstate the old behaviour somehow. In the meantime, I'm hoping you can pin your project to Mocha v1.9 to avoid the problem until we come up with a solution. |
Thank you for the extra info @floehopper. Even though I was trying to avoid it, we will be locking Mocha at 1.9 for now. Indeed, more explanation would help as I still don't get why the new behavior makes more sense. The idiom One important note: the mocking in this test is there to prevent expensive API calls, not to set expectations on the invocations. In RSpec terminology, I need Anyway, I wrote same tests using RSpec mocking and got a similar error. So RSpec seems to agree with you all, but I would not take that as the definitive answer: the very reason we chose Mocha over RSpec for mocking was because Mocha was simply more powerful at the time. So let's not consider RSpec the reference behavior and jump to conclusions. And indeed, until 1.9, Mocha was still more powerful as it allowed us to support the use case I describe in this issue. For reference, here is the RSpec implementation: # my_service_spec.rb
describe MyService do
it "returns" do
allow(MyService).to receive(:expensive_method).and_return("returned")
expect(MyService.expensive_method).to eq "returned"
end
it "yields" do
allow(MyService).to receive(:expensive_method).and_yield("yielded")
MyService.expensive_method do |yielded|
expect(yielded).to eq "yielded"
end
end
it "both returns and yields" do
allow(MyService)
.to receive(:expensive_method)
.and_yield("yielded")
.and_return("returned")
call_result = MyService.call
expect(call_result).to include("returned")
expect(call_result).to include("yielded")
end
end This gives me the following:
|
👍
I will come back to you about this.
Thanks for the clarification.
Thanks for trying this. That's useful information. I've just opened #437 which provides a configuration option to revert to the v1.9 behaviour, albeit with a deprecation warning. If there's any way you could try out the |
I'm a bit sick at the moment, so I apologise if I haven't got my head round this properly. 🤒 I think the new behaviour is inline with the rest of Mocha in the sense that stubbing a method is intended to provide a deterministic/explicitly-defined alternative implementation for the stubbed method. The old behaviour meant that the stubbed method would yield in one case, but not in the other and could thus be regarded as not so deterministic/explicitly-defined. The new behaviour might be useful to some people, because if they're getting a However, given your comments, I'm wondering whether what we're actually missing is some way to match invocations that do or don't have a block given, something along the following lines: def test_both_returns_and_yields
MyService
.stubs(:expensive_method)
.with_block_given
.yields("yielded")
.returns("returned")
MyService
.stubs(:expensive_method)
.with_no_block_given
.returns("returned")
call_result = MyService.call
assert_includes call_result, "returned"
assert_includes call_result, "yielded"
end I realise that's a lot more verbose, but I like the fact that it's more explicit. And if there are common cases we can always come up with shortcuts for those. I haven't thought this through in a lot of detail and in particular I haven't thought about whether it would be possible to implement this in Mocha, but on first glance I think it should be. Another alternative approach might be to provide a new What do you think? @nitishr Do you have any thoughts on this? |
Thanks a lot for the detailed explanation @floehopper ! This last part in particular was the "aha!" moment for me. I was just thinking in terms of stubbing, but from the expectation point of view, the new behavior does make sense.
Awesome, thanks for addressing this so quickly! I will try the
Oh, I like these Thanks again @floehopper, and get well soon! |
@yemartin Thanks. I will try to get these changes (or something very similar) into a patch release as soon as I can. |
I have 2 contradicting lines of reasoning about this, and I'm still trying to make up my mind about which one leads to a more logical and coherent (and ideally simpler) outcome. On the one hand, in response to
I'm not sure mocha is the the right tool for high level integration tests with an unknown/unpredictable/uncontrollable sequence of method calls. After all, we don't allow conditional return values through the On the other hand, I recognize we do support parameter matchers to specify different responses to different parameters. Perhaps you could consider blocks (or the presence or absence of them) to be a kind of parameter matching, thereby matching different expectations in each case. I guess @floehopper's suggestion/proposal follows that reasoning. I'd find it helpful, @yemartin, to understand your situation/use case in a bit more detail in order to solidify our (or at least my) theory of mocha as a tool and it's usage (actual, recommended, supported, discouraged, prohibited, etc.). Thoughts, @yemartin or @floehopper? |
Thank you for the input @nitishr. I will try to give a bit more details about what we are actually doing but it will take me some time to summarize without going into unnecessary details. So first, let me clear two important point: 1. Our return / yielded values are the same on every call, so the use case should fit Mocha
I made the yielded and returned values different in the code sample for clarity, but in our actual test, we return / yield the same value, on every call. It is only the sequence of invocations that is intricate, and maybe even unpredictable. And looking at the
This allows for an unpredictable sequence of invocations, so I believe our use case fits Mocha well. 2. I am not sure the new behavior is correct anymoreThinking about it more, I am taking back what I wrote yesterday about "getting it". It is a matter of semantics, the difference between MyService
.expects(:expensive_method) # <--- this
.yields("a value")
.returns("a value")
.times(3) Then indeed, I am making an explicit expectation. I specify exactly the invocation, and I would want to get that But I am using So I am going back to: maybe the change in #382 needs to be reevaluated. Would it make sense to have the old behavior when using |
@yemartin Just to let you know, I've released v1.10.2 which includes a new configuration option ( |
There's some more detailed documentation here. |
@nitishr, @yemartin Thanks for your input. I'm currently spiking on a slight variation of what I mentioned earlier. The variation is adding new parameter matchers to allow matching on the presence or absence of a block, e.g. |
I'm not sure this ☝️ is going to fly, because it interferes too much with standard parameter matchers. So I'm now exploring the |
@nitishr I think both your lines of reasoning are legitimate. Mocha's sweet spot definitely isn't high-level integration/acceptance tests. Having said that from a pragmatic point-of-view, we have to accept that many people have been using it in this way for years. I think this is why I'm drawn towards the idea of enhancing the existing parameter matching to include any block passed in. This doesn't seem inconsistent with the way Mocha currently works and would support @yemartin's use case. |
I think there's something in what you're saying, although I think it's more to do with the default parameter matching ( |
@yemartin Mocha v1.11.x introduces the |
In Mocha v1.10.0 some undocumented behaviour changed without any prior deprecation warning. * The behaviour of `API#mock`, `API#stub` and `API#stub_everything` when called with a symbol as the first argument. * The behaviour of `Expectation#yields` and `Expectation#multiple_yields` when the stubbed method is called without a block. This caused problems for some developers and so in v1.10.2 this undocumented behaviour was reinstated along with some deprecation warnings. The `reinstate_undocumented_behaviour_from_v1_9` configuration option (defaulting to `true`) allowed developers to address the deprecation warnings and to switch to the new behaviour. Since Mocha v1.10.2 was released nearly 3 years ago and we're about to do a major version bump, it's safe to remove the deprecated behaviour and the associated configuration option. See #436, #438. Closes #569.
.yields()
failing when the caller does not provide a block breaks legitimate testsI have a library with an expensive method (API calls) that both yields and returns. In some tests, both the yielding and the returning aspects are used, so I need to stub both. This used to work fine in 1.9.0, but broke when upgrading to 1.10.1. I traced it down to this in the 1.10.0.alpha release notes (emphasis mine):
But my calls to
.yields
are legitimate and necessary, I cannot just remove them. So I am left with broken tests, and I am not sure how to fix them. Maybe the change in #382 needs to be reevaluated?How to reproduce
Here is a code sample that illustrates how one may need both
.returns
and.yields
in the same test:With mocha 1.9.0:
With mocha 1.10.1:
The text was updated successfully, but these errors were encountered: