-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Jasmine clock + Promise unintuitive #1659
Comments
Promises resolving asynchronously is even more complicated than you indicate here. Most promise libraries actually use a few other tricks to do asynchronous callbacks for resolution and rejection with less overhead that If I'm understanding you correctly, I think Jasmine actually does do some of the things that you suggest. If you schedule a describe('timeouts', function() {
beforeEach(() => { jasmine.clock().install() });
afterEach(() => { jasmine.clock().uninstall() });
it('does some stuff', function() {
var thing1 = jasmine.createSpy('thing1');
var thing2 = jasmine.createSpy('thing2');
thing1.and.callFake(() => setTimeout(thing2, 0));
setTimeout(thing1, 0);
expect(thing1).not.toHaveBeenCalled();
expect(thing2).not.toHaveBeenCalled();
jasmine.clock().tick(0);
expect(thing1).toHaveBeenCalled();
expect(thing2).toHaveBeenCalled();
});
}); This fails because describe('timeouts', function() {
beforeEach(() => { jasmine.clock().install() });
afterEach(() => { jasmine.clock().uninstall() });
it('does some stuff', function() {
var thing1 = jasmine.createSpy('thing1');
var thing2 = jasmine.createSpy('thing2');
thing1.and.callFake(() => setTimeout(thing2, 0));
setTimeout(thing1, 0);
expect(thing1).not.toHaveBeenCalled();
expect(thing2).not.toHaveBeenCalled();
jasmine.clock().tick(1);
expect(thing1).toHaveBeenCalled();
expect(thing2).toHaveBeenCalled();
});
}); This passes. This functionality is the current defined functionality for the Jasmine clock (we even have a comment denoting): // checking first if we're out of time prevents setTimeout(0)
// scheduled in a funcToRun from forcing an extra iteration The reasoning for this seems to me to be so you can Hope this helps. Thanks for using Jasmine! |
Thank you very much for the response. I hadn't considered behavior with Please let me know if there's anything I can clarify. |
I agree that you don't want to write a spec that knows about the promise depth in the code under test. That being said, if you're testing code that uses promises you probably want to use the new-ish promise support in Jasmine (either I would also be happy to review a pull request for the docs to give an example of providing a promise implementation that completes synchronously/on demand. |
I was looking at Jasmine code, and it looks like describe('timeouts', function() {
beforeEach(() => { jasmine.clock().install() });
afterEach(() => { jasmine.clock().uninstall() });
it('does some stuff', function() {
var thing1 = jasmine.createSpy('thing1');
jasmine.clock().tick(1);
setTimeout(thing1, 0);
expect(thing1).not.toHaveBeenCalled();
});
}); To make testing async code a little easier, one option would be to have a new method so thing1 would be called in that case: describe('timeouts', function() {
beforeEach(() => { jasmine.clock().install() });
afterEach(() => { jasmine.clock().uninstall() });
it('does some stuff', function(done) {
var thing1 = jasmine.createSpy('thing1');
// new function call, records current clock value for reference
jasmine.clock().markTimeForAsync();
jasmine.clock().tick(1);
// fake setTimeout sees it is 1 past the reference time, and runs thing1 immediately
setTimeout(thing1, 0);
expect(thing1).toHaveBeenCalled();
// Another example, this would work also
var thing2 = jasmine.createSpy('thing2');
jasmine.clock().markTimeForAsync();
Promise.resolve(() => setTimeout(thing2, 0)).then(() => {
expect(thing2).toHaveBeenCalled();
done();
});
jasmine.clock().tick(1);
});
}); I don't think I was previously aware of the |
It sounds like what you're looking for is a way for a previous call to |
Yes, one way to make Jasmine clock + Promise more intuitive would be for a previous call to Promise.resolve().then(() => {
jasmine.clock().tick(30000);
}); but if there's another Promise added to the source code in the future, that will break. And I find it to be unintuitive that I need to advance the clock like that in the first place. |
The Jasmine clock is fairly complicated already, and I don't really see a good way to add functionality like this without surprising other users while still being able to figure out what the clock is going to do in any given circumstance. I would either see what kind of use you can make out of actually Hope this helps. Thanks for using Jasmine! |
I just realized that the code in the original issue wasn't failing as I said it was, so I fixed it and made the example slightly more complicated to illustrate my issue. As far as our use-case, we've got a auto-login function that needs to load some items saved in Storage. We've got the Storage interface mocked out with Promises that resolve, and HTTP mocked out with Observables. If you're loading a username, server name, and authentication token, then performing a HTTP call to check if it's working, then you've got multiple async things and a timeout on the last item. Thank you very much for at least considering my issue. I really appreciate the work you do with Jasmine. You can close this issue if you don't think it's worth addressing. |
Unfortunately, I can't really think of a good way to incorporate some of this logic without impacting the ability to reason about the clock. I could see maybe adding a note to the documentation to clock about interactions with promises, but the asynchronous nature of promises is something that I would expect users using promises to know on their own, since this isn't due to anything that Jasmine is doing. I'm going to close this. But please feel free to open a PR for a note that seems like it makes sense either here in the JSDoc comments or in the documentation repo. |
I learned a little bit more about Jasmine, RxJS's delay operator, and Angular. Specifically, if I used "delay()" from RxJS, it would never fire even if I had jasmine.clock() installed and I used jasmine.clock().tick() to advance the clock past where it should fire. It turns out that "delay()" looks for a scheduled Date to fire as well as using setInterval(). Since jasmine.clock().tick() doesn't advance the clock unless you also call jasmine.clock().mockDate(), it never fires. In trying to figure out what I was doing wrong for that issue, I found Angular's fakeAsync(), tick(), and flushMicrotasks() functions. Those seem to work the way I was hoping Jasmine would work (tick() will trigger Promises to resolve as well as timers as well as advance Date.now()). |
For faking RxJS time, you can use the 2nd argument of |
I didn't know about the VirtualTimeScheduler, thank you for bringing that up. I think I'd still prefer cleaner helper functions, rather than spying on RxJS operators to substitute the VirtualTimeScheduler. Also, a new scheduler for RxJS wouldn't address the issues with Promises. |
It took me a while to figure out how to test a function that used a Promise and a timeout with Jasmine.clock(), so I figured I'd log this issue with my solution and a request to change something. I'm basically reposting issue #710 and #1282 and asking for a better solution than "this is more of a how-to-unit-test question rather than a jasmine issue".
An example simplified function to unit test:
And the unit test (nonworking):
Expected Behavior
tick() or some other function should trigger the timeout.
Current Behavior
jasmine.clock().install() disables timing out until tick is called. Tick must be called after the timeout is set, which may be set in an async context and inaccessible from the unit test.
Possible Solution
Replace the tick() line with this:
Or, mark the function provided to it() as async and replace the subscribe( line with:
Alternately, as mentioned by the other issues and StackOverflow post, override Promise to be synchronous, or save off the original setTimeout() function before installing jasmine clock. Perhaps Jasmine could:
jasmine.clock()
is installed, that could be a reference time, so if it was ticked forward, it would then trigger any timeouts that should've been triggered, even if those timeouts were set after thejasmine.clock().tick()
call.jasmine.clock().tick(300).withAsyncLevels(3)
that would result in the codeContext
I spent multiple days trying to figure out why my code appeared to be ceasing execution in the chain of promises and observables I was attempting to test, eventually narrowing it down to an issue between interactions of Promise + Jasmine + Observable timeout operator. Then learning that
jasmine.clock().tick()
won't trigger timeouts that haven't been set yet, Promises are defined to be always async, and Observables may be async or sync depending on how they're used.Your Environment
The text was updated successfully, but these errors were encountered: