Skip to content
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

Add a how-to guide for speeding up time with lolex #1931

Merged
merged 8 commits into from Jun 28, 2019
Merged

Add a how-to guide for speeding up time with lolex #1931

merged 8 commits into from Jun 28, 2019

Conversation

orlangure
Copy link
Contributor

Purpose (TL;DR) - mandatory

This PR adds a how-to guide about current ways to control promises/async functions with lolex (#1898).

In a nutshell, there are a few examples: a regular function, an async function, and a Promise (walk into a bar). These things are tested in different ways to demonstrate what we can and cannot control.

How to verify - mandatory

  1. Check out this branch
  2. Follow the steps described here to build the documentation website locally (using jekyll)
  3. Navigate to http://localhost:4000/ -> "How to" -> "How to speed up time with lolex"

@coveralls
Copy link

coveralls commented Oct 13, 2018

Pull Request Test Coverage Report for Build 2909

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at 94.186%

Totals Coverage Status
Change from base Build 2903: 0.0%
Covered Lines: 1660
Relevant Lines: 1729

💛 - Coveralls

@benjamingr
Copy link
Member

Hey, thank you so much for following up on our email discussion - will take a look promptly :)

@@ -0,0 +1,128 @@
---
layout: page
title: How to speed up time with lolex
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the title be "How to test async functions with fake timers" perhaps?

title: How to speed up time with lolex
---

With lolex, testing code that depends on timers is easier, as it sometimes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With lolex -> "With fake timers (lolex)"?

});
```

We could expect similar behavior from a promisified timeout:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could - "one might"?

Also - maybe "One might expect to be able to use a similar approach to test an async function using promises:"?

};
```

Trying a naive approach:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about "naive", maybe "One may try"?

assert.equal(result, 42); // PASS
});

// or using Mocha's promises
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe "Or without using an async function" since both versions are using Mocha's promise return value support?

});
```

Although `async` functions cannot be tested synchronously, we can test Promises
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add "(yet)" after "cannot"? We're talking about it :)

```js
// maker.js
module.exports.fulfillAfterOneSecond = () => {
return new Promise(fulfill => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is resolve and not fulfill

Copy link
Contributor

@fatso83 fatso83 Oct 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To nitpick further: I think what Benjamin is saying is not that you have to call your callbacks "resolve", but that "fullfill" is a somewhat misleading name for the resolving callback, as a promise is also fulfilled if you reject it :-)

};
```

`fulfillAfterOneSecond` resolves to 42 after one second, just like
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like: By returning the promise from mocha's test and making it an async test we can still progress the internal timers using lolex's fake timers - so we only have to wait the time it takes the promise to resolve which is almost instant.

Or something of the sort?

```

The above test passes immediately, without the 1 second delay. This is because
we do not try to hijack `then()` call, but use Mocha's native ability to test
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"we do not try to" "while we do not"?

Copy link
Contributor

@fatso83 fatso83 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Thank you for a great little gem! I think this is a good start, but there were some unclear points that might need addressing. Ask if you find anything unclear or if my comments confuse you/seem incorrect.

Have a great weekend :)

it('should return 42 after one second', () => {
const promise = maker.asyncReturnAfterOneSecond();

// this call does not really speed up anything
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just creates confusion, if it is supposed to show a way of doing the same as above, but without ES2015 syntax. Line 86 and 87 can be deleted.

```

The above test fails, since `asyncReturnAfterOneSecond` is an `async` function
that returns a Promise, and it is currently impossible to control the Promises'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the Promise's (definite singular) or promises' (indefinite plural)

```

`fulfillAfterOneSecond` resolves to 42 after one second, just like
`asyncReturnAfterOneSecond`, but it is testable in a synchronous way:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, it's not synchronous. the test is still asynchronous, as the assert call is done in a microtick after the function exits. The reason fulfillAfterOneSecond resolves (almost) immediately using clock.tick and asyncReturnAfterOneSecond is not affected by clock.tick has to do with how setTimeout is resolved. In asyncReturnAfterOneSecond you bind the original value of setTimeout using util.promisify(). That means it has a reference to the original setTimeout which will not be affected by anything lolex does. async really has nothing to do with it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is an assumption lolex.install() was called prior to anything - but I think it makes a ton of sense to make it explicit as part of the test (that is, add a lolex.install() part above)

```

The above test passes immediately, without the 1 second delay. This is because
we do not try to hijack `then()` call, but use Mocha's native ability to test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this section. You didn't try to hijack the then call in earlier tests? Mocha's built-in promise support is just syntactic sugar to avoid passing in a callback and there isn't much native stuff going on. I'd replace "native ability to test Promises" with "built-in support for Promises"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed about "built-in support for promises"


The above test passes immediately, without the 1 second delay. This is because
we do not try to hijack `then()` call, but use Mocha's native ability to test
Promises, while speeding up their resolution.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it seems like it is Mocha that is speeding up their resolution. I think it would be clearer if you changed it to "while using lolex to speed up the test".

we do not try to hijack `then()` call, but use Mocha's native ability to test
Promises, while speeding up their resolution.

Although it is currently impossible to speed up `async` functions, or even
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't correct. You can speed up async functions. It's just syntactic sugar-coating for promises. Anything you can do with promises is possible with async and vice versa. Unless you can inform me otherwise?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's easier to speed up promises because you can override the promise library to one that uses a scheduler

Speeding up async functions without invoking the native %RunMicrotasks (which requires an addon) or using a require('binding') with an undocumented internal API requires transpiling your async functions to coroutines.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but I didn't mean overriding the scheduler for the Promises, but rather the statement that implied that one cannot speedup async tests that use timers. It seemed as if the statement said that if you have an async test using setTimeout (for instance), then it would not be affected by clock.tick calls, which would be incorrect.

Just the fact that we are arguing the meaning might be a hint that this sentence should be rephrased somehow :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

we do not try to hijack `then()` call, but use Mocha's native ability to test
Promises, while speeding up their resolution.

Although it is currently impossible to speed up `async` functions, or even
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not currently impossible - just very very hacky :) I would add here the conclusion maybe:

"Although it is currently impossible to speed up the thens async functions run it is still possible to speed up timers and write meaningful sped-up tests with code using async functions." or something of the sort.

Up to you.

@benjamingr
Copy link
Member

Thank you for working on this!

.gitignore Outdated
@@ -5,3 +5,4 @@ coverage/
.idea
.sass_cache
_site
*.swp
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this. This is not specific to this project.

Use git's global ignore so you don't need to add vim knowledge to every repo you touch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.
How is it different from .idea though?😼

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think it makes sense to either allow these or remove them all but no strong feelings

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong feelings either. If .swp doesn't belong here, neither does .idea.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't review the initial commits and I have no idea what .idea is for in the first place. I think it makes sense for the files in a repo to be specific to the repo instead of specific to the people that edited the repo. If .idea is specific to some editor and/or OS, then I think it should be removed as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.idea -> IntelliJ IDEA

@orlangure
Copy link
Contributor Author

Thanks for your comments, I'm in the middle of fixing the issues you guys raised.
Will post a second draft sometime soon.

@benjamingr
Copy link
Member

@orlangure thanks, we're looking forward to it :)

@orlangure
Copy link
Contributor Author

I ended up changing a lot of text and some of the examples, so not all your suggested changes made it into this draft. But they definitely made things more clear to me and allowed to provide more clear examples.


```js
module.exports.asyncReturnAfterOneSecond = async () => {
// util.promisify is not used deliberately
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need your help here. I think this comment should not be in a guide, but without it it looks like a mistake to not use util.promisify.

When it is used though, the test fails.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When it is used though, the test fails.

Make it a proxy to util.promisify or call clock.install() before util.promisify is called (because then it holds a reference to the original setTimeout):

module.exports. asyncReturnAfterOneSecond = async () => {
  const setTimeoutPromise = util.promisify(setTimeout); //
  // ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, even with lolex.install call before util.promisify, tests of this function fail.

What did you mean by proxying somehting to util.promisify? I'll check it as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to be fixed in lolex. I'll make an issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference, the issue is sinonjs/fake-timers#227

@orlangure
Copy link
Contributor Author

Can this guide be reviewed again before the issue is fixed? I guess most of it does not depend on that bug in lolex.

@benjamingr
Copy link
Member

Yeah, this is my bad and I'll fix it in lolex in the coming week (hopefully, it's on my plate). Then I think we can merge this in :)

@stale stale bot added the stale label Jan 12, 2019
@orlangure
Copy link
Contributor Author

Still waiting for sinonjs/fake-timers#227 to be reviewed and merged. Bumping for now.

@stale stale bot removed the stale label Jan 13, 2019
@stale stale bot added the stale label Mar 14, 2019
@sinonjs sinonjs deleted a comment from stale bot Mar 14, 2019
@stale stale bot removed the stale label Mar 14, 2019
@sinonjs sinonjs deleted a comment from stale bot Mar 14, 2019
@fatso83
Copy link
Contributor

fatso83 commented Mar 14, 2019

@benjamingr Any chance of looking at the PR blocking getting this in? Yuri has kind of been waiting since October.

@stale
Copy link

stale bot commented May 13, 2019

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label May 13, 2019
@fatso83 fatso83 added the pinned label May 14, 2019
@stale stale bot removed the stale label May 14, 2019
Copy link
Contributor

@fatso83 fatso83 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@orlangure I don't see why we should keep this hanging. I know sinonjs/fake-timers#227 has been stalled for some time, but this is just affecting a single line in this whole commit, and the current documented way works. Duplicate efforts have started appearing (#2054), so I'd like to focus the efforts :-)

If you could just update the minor spelling issue I'm all good with this. My suggestion on the comment wording is optional.

});
```

While returning a Promise from the Mocha’s test, we can still progress the timers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
While returning a Promise from the Mocha’s test, we can still progress the timers
While returning a Promise from Mocha’s test, we can still progress the timers


```js
module.exports.asyncReturnAfterOneSecond = async () => {
// util.promisify is not used deliberately
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// util.promisify is not used deliberately
// Using util.promisify would look nicer, but there is a lolex issue blocking this at the moment: https://github.com/sinonjs/lolex/pull/227

@orlangure
Copy link
Contributor Author

@fatso83 Done, thanks for pushing this forward.

@fatso83 fatso83 merged commit 78964d7 into sinonjs:master Jun 28, 2019
@fatso83
Copy link
Contributor

fatso83 commented Jun 28, 2019

Thank you! This should go live at https://sinonjs.org/how-to in a few minutes.

@orlangure orlangure deleted the testing-promises-with-lolex branch June 28, 2019 12:11
franck-romano pushed a commit to franck-romano/sinon that referenced this pull request Oct 1, 2019
* Add a how-to on speeding up time with lolex
* Explain weird setTimeoutPromise usage in comments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants