-
-
Notifications
You must be signed in to change notification settings - Fork 771
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
Overridable yields
vs sequential behavior
#244
Comments
Here's my thoughts: The change of semantics in 1.5 was absolutely not intended, but I'd failed to have automated tests for this behavior and didn't pay close enough attention. Even though 1.5 has been out for a while I've been thinking of revoking it and replacing it with 1.6 which restores the original behavior. However, time flies... @domenic mentioned he was going to look into a possible "best of both worlds" solution - any news on that? |
Well, I already fixed the second part of #231 and I think that was all I meant to do---best of both worlds is a bit more than I promised :). If we can agree on what the desired behavior is, I'm happy to come up with something that does both. E.g. maybe |
What, you're no magician? :)
This! If you do it, I'll be very grateful, otherwise I'll eventually get to it myself. |
Yes, I'd also be very much in favor of |
OK! Sounds like a weekend plan. Hopefully this weekend, but you know how these things go. |
👍 for |
Thinking about this a little more, especially things like var foo = sinon.stub();
foo.yields(4).returns(true);
foo.thenYields(5).thenReturns(false); // foo yields, next call returns false While this surely is a rare case, it might point out a weakness in the proposed API. As an alternative, one could consider introducing a separate foo.yields(4).returns(true);
foo.then().yields(5).returns(false);
// While the above example amounts to
foo.yields(4).returns(true);
foo.then().yields(5);
foo.then().returns(false); Making it work in conjunction with the // Defining two independent sequences
foo.withArgs(1).returns(2);
foo.withArgs(1).then().returns(3);
foo.withArgs(2).returns(4);
foo.withArgs(2).then().returns(5);
foo(1); // => 2
foo(2); // => 4
foo(1); // => 3
foo(2); // => 5
// Argument based case distinction in sequence
foo.returns(0);
foo.then().withArgs(1).returns(3)
.withArgs(2).returns(4);
foo('anything'); // => 2
foo(1); // => 3 Given the apparent complexity, I am still unsure whether this is of more than academic interest. I have yet to find a need for such sequencing functionality and originally opened this issue because even the much simpler existing sequencing functionality tripped me in my day to day work. So maybe others that have actually used the sequence aspects of Best |
Thank you for the detailed write up. I agree that this might be something that is not used very frequently. However, I also see a value in this feature. Now that I read your use case examples, a different way of approaching the problem came to mind. foo.returns(2);
foo.onCall(1).returns(3); This would solve your foo.onCall(2).yields('foo').returns(true); and it could even be used in this way: foo.withArgs(1).onCall(3).returns(5); I also imagine this to be a simpler implementation. What do you think? |
@mantoni I like that quite a lot, and avoiding the combinatorial explosion is a big plus. @cjohansen thoughts? |
So just to make sure I fully understand: The stub in your first example would return Moreover, what is the expected behavior if I skip a number (e.g. call I'm not really sure whether it is desirable to explicitly write out the sequence of indexes. The only advantage I see is the possibility to construct a stub behavior of a call in multiple statements, by repeatedly calling In terms of implementation complexity I might be missing something though. |
It would be zero based, just like
It would fall back to whatever was programmed by default. This would be in line with
Hm, I was more seeing it this way: I have a default behavior for a method and I want something different to happen on, let's say, the 4th call. That would be tedious with |
Ah ok, I wasn't even aware of |
True. For calls there is |
This would definitely be a great fit with the rest of the API. I like this API better than my initial proposal, even more so since on second thought |
Ok, maybe then its time to start talking about how to best implement the above ideas. Some thoughts:
Thoughs? |
I'm sorry, but I have problems following your thoughts. My first attempt would have been to do the same thing that we did for I might as well overlook something here. Not sure. |
So |
Oh yes. You are right about that. Does not make sense on spies. For assertions, one would use |
I have run into a similar situation so, all of a sudden, this topic has taken on new significance for me. I have a stub that needs to return different values on different calls (to simulate an environment change during the test - specifically, a transition from online to offline). Tim, I like your concept of Is anyone moving forward on this? I can make a local change to allow me to specify multiple |
I like the behavior idea, and would be all for it if someone like @tf wanted to take over this feature from me :). |
Now that I looked at the code in the right place, I'm also convinced. Sorry @tf for the confusion. |
I'll take a look. Though, if I do not get around to it during the weekend, it may end up being some time late next week since we will be pretty swamped at work. |
@tf No one is in a rush, take your time ;) |
I've started refactoring out
I'd be glad if somebody could take a look at the code. If things are moving in the right direction, I'd be happy to proceed. |
@tf looking good! Very nice to have this separated out. Many of the functions on behavior do mostly the same thing (setting four properties). Maybe these could be set by a single "private" function, so we can reduce the duplicated boilerplate. Other than that, I like this direction a lot.
I appreciate your disciplined approach. Still, it does sound like those tests need some "loosening up".
Given that the API surface has grown quite a bit since I started this project, it might make sense to separate the tests this way. It's a good idea, anyway.
I think it looks good. Maybe @mantoni wants to contribute a second opinion? :) |
I also like the direction this is going. Here are some thought I had while reading through the implementation: One thing I found a little difficult to read is the way The question about where the test cases should go is a good one. My personal opinion on this topic is, that only the API should be tested. I would not consider However, I understand that this can be seen as "integration testing" if you make a decision between unit and API testing. I also agree that the test cases are grown a lot and could be split up - but that's a different story. I'll support whatever decision is made on this - just wanted to throw in my thoughts and hear what you think about it. Other than that: great work @tf - this is going to be a big improvement. |
I agree on this. However, when underlying details are shared you have to decide between unit testing components that aren't really part of the API or duplicating tests at a higher level (where they are visible). FWIW, I find it generally hard to strike the right balance in this regard. |
True. I already found myself copy-paste-adjusting test cases - that does show something :) |
That makes sense. Is that what happens, too? :) |
Yes. |
Sounds like the development of this is progressing or may even be complete. Has any thought been given toward when this will be merged into the code base? |
Is it complete? Sorry for falling out here. |
Just yesterday I discovered an issue which probably also exists in the current implementation, but which I'd like to iron out before submitting a pull request. Basically fakes do not inherit behavior. var stub = stub();
stub.returns(1);
stub.withArgs(1).onFirstCall().returns(2);
stub(1) // => 2
stub(1) // => undefined, while 1 would be expected I guess stubs need to propagate behavior to their fakes or fakes need to know their parent stubs. When this is fixed the missing stub.onFirstCall()
.withArgs(1).returns(0)
.withArgs(2).returns(1); is the same as stub.withArgs(1).onFirstCall().returns(0);
stub.withArgs(2).onFirstCall().returns(1); So if we let the behavior know its index in the list of behaviors, I try to look into these things as soon as possible, which might not be before the end of the week. |
Ok, I realize this has been lying around for far too long and this discussion has already grown to an enormous length. I have it working now on my machine, but there are still two things I want to discuss with you guys before I prepare a pull request. Issue 1 The way I tweaked it, fakes inherit behavior from their parent stubs. Moreover stub.returns(1);
stub.onFirstCall().withArgs(5).returns(2);
stub(5) // => 2
stub(5) // => 1 As a side effect of this behavior inheritance, resetting the behavior of a fake now makes matching calls fall back to the parent stub again: stub.returns(1);
stub.withArgs(5).returns(2);
stub.withArgs(5).resetBehavior();
stub(5) // => 1, used to be undefined To me this seems way more intuitive now, but I'm not sure if it breaks tests for someone. Issue 2 The seconds issue I've already mentioned above: var stub = sinon.stub().returns(0).onFirstCall().returns(1);
stub(1); causes an exception. On the other hand, var stub = sinon.stub().returns(0).withArgs(6).returns(1);
stub(0); // => 1 not 0 Once we have those sorted out, I'll be glad to prepare a pull request. Best |
Since this issue seems to be getting some attention lately, I'd like to bump it again. If we can agree that the two issues above are acceptable, I'd try to rebase my branch against master. |
Thanks for taking it up again @tf. I think "Issue 1" is more a feature than an issue. It makes the behavior act more intuitive. "Issue 2" is something we should address in @cjohansen What's your opinion? |
|
… behavior sequences (sinonjs#244)
* store a reference back to the parent stub on a fake * if a fake neither has a behavior for the current call index nor a default bahavior, delegate to the parent * as a result calling resetBehavior on a fake makes it fall back to its parent behavior again
* pass callIndex to behavior * delegate withArgs back to stub invoking matching onCall
otherwise delegation of onFirstCall().withArgs(...) to withArgs(...).onFirstCall() creates a blank behavior for first call. Calls not matching the specified args do not fall back to default stub behavior then.
Alright. The rebase applied cleanly. I've added a bunch more tests including There is obviously much room for improvement and refactoring, but I just wanted to push it out there now. Please take a look, play around with it to see if there are cases I might not have thought about. |
Bump. Shall I make a pull request out of this, so the changes can be reviewed more easily? I'd hate for this to go stale. |
PR away! I'm AFK until next week. Will have a look then.
|
Great. Submitted the PR. |
Closing this issue since the PR #338 was merged. |
Hi,
in version 1.5.0 the semantics of calling
yields
multiple times changed from overriding former instructions to recording a sequence of values to be yielded sequentially (see #157).While the new behavior might be desirable in some cases, it makes one of our frequently applied test patterns almost impossible to use. We like to encapsulate the construction of commonly used stub objects in test helpers:
Overriding the default behavior of individual stub methods allows each test to focus on the setup relevant to its intent. Since the change, this is no longer possible forcing the test to duplicate the complete stubbing setup if it needs to diverge from the default.
I've looked into different ways to circumvent this issue, but have not yet come up with a side effect free solution.
While I have to say the old API made more sense to me, I guess others are already depending on these new semantics. So I'd suggest adding a new way of calling
yields
which will be overridden by sequential calls. Something along the lines ofyieldsDefault
, while this might not be an intuitive name. Or an option onstub
?I'd be really interested to hear your thoughts on this.
Cheers
Tim
The text was updated successfully, but these errors were encountered: