From feb09d113bda1b04562f87fcf7e6d88b0667bb04 Mon Sep 17 00:00:00 2001 From: yury Date: Sat, 13 Oct 2018 21:18:45 +0300 Subject: [PATCH 1/7] Add vim swp files to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 40ab5d5dd..80c71829b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ coverage/ .idea .sass_cache _site +*.swp From cb722acb86bc9922c6c33f0068de66fa6cc574fd Mon Sep 17 00:00:00 2001 From: yury Date: Sat, 13 Oct 2018 21:19:16 +0300 Subject: [PATCH 2/7] Add a how-to on speeding up time with lolex --- docs/_howto/lolex-async-promises.md | 128 ++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 docs/_howto/lolex-async-promises.md diff --git a/docs/_howto/lolex-async-promises.md b/docs/_howto/lolex-async-promises.md new file mode 100644 index 000000000..3818f7f0c --- /dev/null +++ b/docs/_howto/lolex-async-promises.md @@ -0,0 +1,128 @@ +--- +layout: page +title: How to speed up time with lolex +--- + +With lolex, testing code that depends on timers is easier, as it sometimes +becomes possible to skip the waiting part and trigger scheduled callbacks +synchronously. Consider the following function of a maker module (a module +that makes things): + +```js +// maker.js +module.exports.callAfterOneSecond = callback => { + setTimeout(callback, 1000); +}; +``` + +We can use lolex to verify that `callAfterOneSecond` works as expected, but +skipping that part where the test takes one second: + +```js +// test.js +it('should call after one second', () => { + const spy = sinon.spy(); + maker.callAfterOneSecond(spy); + + // callback is not called immediately + assert.ok(!spy.called); + + // but it is called synchronously after the clock is fast forwarded + clock.tick(1000); + assert.ok(spy.called); // PASS +}); +``` + +We could expect similar behavior from a promisified timeout: + +```js +// maker.js +const util = require('util'); +const setTimeoutPromise = util.promisify(setTimeout); + +module.exports.asyncReturnAfterOneSecond = async () => { + await setTimeoutPromise(1000); + return 42; +}; +``` + +Trying a naive approach: + +```js +// test.js +it('should return 42 after one second', () => { + // result will hold a Promise after the next call + const result = maker.asyncReturnAfterOneSecond(); + + clock.tick(1000); + + // the Promise was not resolved, + // even though we moved the time forward + assert.equal(result, 42); // FAIL +}); +``` + +The above test fails, since `asyncReturnAfterOneSecond` is an `async` function +that returns a Promise, and it is currently impossible to control the Promises' +forced resolution. **Their `then()` function always runs asynchronously**. + +It doesn't mean that `async` functions and Promises cannot be tested though. +The most intuitive way to test the above `asyncReturnAfterOneSecond` is to +simply wait for one second: + +```js +// test.js + +// using async await +it('should return 42 after one second', async () => { + const result = await maker.asyncReturnAfterOneSecond(); + assert.equal(result, 42); // PASS +}); + +// or using Mocha's promises +it('should return 42 after one second', () => { + const promise = maker.asyncReturnAfterOneSecond(); + + // this call does not really speed up anything + clock.tick(1000); + + return promise.then(result => assert.equal(result, 42)); // PASS +}); +``` + +Although `async` functions cannot be tested synchronously, we can test Promises +that are resolved in `setTimeout`. Consider the following function, that has the +same functionality as `asyncReturnAfterOneSecond`: + +```js +// maker.js +module.exports.fulfillAfterOneSecond = () => { + return new Promise(fulfill => { + setTimeout(() => fulfill(42), 1000); + }); +}; +``` + +`fulfillAfterOneSecond` resolves to 42 after one second, just like +`asyncReturnAfterOneSecond`, but it is testable in a synchronous way: + +```js +// test.js +it('should be fulfilled after one second', () => { + const promise = maker.fulfillAfterOneSecond(); + + // this call actually makes the resolution quicker + clock.tick(1000); + return promise.then(result => assert.equal(result, 42)); // PASS +}); +``` + +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. + +Although it is currently impossible to speed up `async` functions, or even +Promises that are not resolved directly in `setTimeout`'s callback, there is a +discussion around this limitation. There is a chance that it will become +possible to force `then()` execution in the future, meaning that the things that +are impossible today may become possible at some point. From 2f00e492a144b1f7bc1968a4565123b796cae379 Mon Sep 17 00:00:00 2001 From: yury Date: Tue, 16 Oct 2018 19:34:13 +0300 Subject: [PATCH 3/7] Reword guide title --- docs/_howto/lolex-async-promises.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_howto/lolex-async-promises.md b/docs/_howto/lolex-async-promises.md index 3818f7f0c..cb384f97c 100644 --- a/docs/_howto/lolex-async-promises.md +++ b/docs/_howto/lolex-async-promises.md @@ -1,6 +1,6 @@ --- layout: page -title: How to speed up time with lolex +title: How to test async functions with fake timers --- With lolex, testing code that depends on timers is easier, as it sometimes From 5bb69f17fce26714fbdeca107742ddc7b7f6b83b Mon Sep 17 00:00:00 2001 From: yury Date: Thu, 18 Oct 2018 22:16:59 +0300 Subject: [PATCH 4/7] Write a second draft for fake timers guide --- docs/_howto/lolex-async-promises.md | 96 ++++++++++++----------------- 1 file changed, 38 insertions(+), 58 deletions(-) diff --git a/docs/_howto/lolex-async-promises.md b/docs/_howto/lolex-async-promises.md index cb384f97c..9f5c84cf5 100644 --- a/docs/_howto/lolex-async-promises.md +++ b/docs/_howto/lolex-async-promises.md @@ -3,7 +3,7 @@ layout: page title: How to test async functions with fake timers --- -With lolex, testing code that depends on timers is easier, as it sometimes +With fake timers (lolex), testing code that depends on timers is easier, as it sometimes becomes possible to skip the waiting part and trigger scheduled callbacks synchronously. Consider the following function of a maker module (a module that makes things): @@ -15,11 +15,16 @@ module.exports.callAfterOneSecond = callback => { }; ``` -We can use lolex to verify that `callAfterOneSecond` works as expected, but +We can use Mocha with lolex to verify that `callAfterOneSecond` works as expected, but skipping that part where the test takes one second: ```js // test.js +before( + lolex.install(); +); +// ... + it('should call after one second', () => { const spy = sinon.spy(); maker.callAfterOneSecond(spy); @@ -33,96 +38,71 @@ it('should call after one second', () => { }); ``` -We could expect similar behavior from a promisified timeout: +The same approach can be used to test an `async` function: ```js -// maker.js -const util = require('util'); -const setTimeoutPromise = util.promisify(setTimeout); - module.exports.asyncReturnAfterOneSecond = async () => { + // util.promisify is not used deliberately + const setTimeoutPromise = timeout => { + return new Promise(resolve => setTimeout(resolve, timeout)); + }; await setTimeoutPromise(1000); return 42; }; ``` -Trying a naive approach: - -```js -// test.js -it('should return 42 after one second', () => { - // result will hold a Promise after the next call - const result = maker.asyncReturnAfterOneSecond(); - - clock.tick(1000); - - // the Promise was not resolved, - // even though we moved the time forward - assert.equal(result, 42); // FAIL -}); -``` - -The above test fails, since `asyncReturnAfterOneSecond` is an `async` function -that returns a Promise, and it is currently impossible to control the Promises' -forced resolution. **Their `then()` function always runs asynchronously**. - -It doesn't mean that `async` functions and Promises cannot be tested though. -The most intuitive way to test the above `asyncReturnAfterOneSecond` is to -simply wait for one second: +The following test uses Mocha's [support for promises](https://mochajs.org/#working-with-promises): ```js // test.js - -// using async await -it('should return 42 after one second', async () => { - const result = await maker.asyncReturnAfterOneSecond(); - assert.equal(result, 42); // PASS -}); - -// or using Mocha's promises it('should return 42 after one second', () => { const promise = maker.asyncReturnAfterOneSecond(); - - // this call does not really speed up anything clock.tick(1000); - return promise.then(result => assert.equal(result, 42)); // PASS }); ``` -Although `async` functions cannot be tested synchronously, we can test Promises -that are resolved in `setTimeout`. Consider the following function, that has the -same functionality as `asyncReturnAfterOneSecond`: +While returning a Promise from the Mocha’s test, we can still progress the timers +using lolex, so the test passes almost instantly, and not in 1 second. + +Since `async` functions behave the same way as functions that return promises +explicitly, the following code can be tested using the same approach: ```js // maker.js module.exports.fulfillAfterOneSecond = () => { - return new Promise(fulfill => { + return new Promise(resolve => { setTimeout(() => fulfill(42), 1000); }); }; ``` -`fulfillAfterOneSecond` resolves to 42 after one second, just like -`asyncReturnAfterOneSecond`, but it is testable in a synchronous way: - ```js // test.js it('should be fulfilled after one second', () => { const promise = maker.fulfillAfterOneSecond(); - - // this call actually makes the resolution quicker clock.tick(1000); return promise.then(result => assert.equal(result, 42)); // PASS }); ``` -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. +Knowing that `async` functions return promises under the hood, +we can write another test using `async/await`: + +```js +// test.js +it('should return 42 after 1000ms', async () => { + const promise = maker.asyncReturnAfterOneSecond(); + clock.tick(1000); + const result = await promise; + assert.equal(result, 42); // PASS +}); +``` + +A callback in the above test still returns a Promise, but for a user it looks +like some straightforward synchronous code. -Although it is currently impossible to speed up `async` functions, or even -Promises that are not resolved directly in `setTimeout`'s callback, there is a -discussion around this limitation. There is a chance that it will become -possible to force `then()` execution in the future, meaning that the things that -are impossible today may become possible at some point. +Although these tests pass almost instantly, they are still asynchronous. Note +that they return promises instead of running the assertions right after the +`clock.tick(1000)` call, like in the first example. **Promises' `then()` +function always runs asynchronously**, but we can still speed up the tests. From 29bb8e1a90e00b918492b3beec4f29f9d97b926e Mon Sep 17 00:00:00 2001 From: yury Date: Thu, 18 Oct 2018 22:17:26 +0300 Subject: [PATCH 5/7] Remove swp from gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 80c71829b..40ab5d5dd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ coverage/ .idea .sass_cache _site -*.swp From 6264595add0c2c5b2760e15b30d90e0bdbadcdfe Mon Sep 17 00:00:00 2001 From: yury Date: Fri, 28 Jun 2019 14:08:43 +0300 Subject: [PATCH 6/7] Fix a typo in lolex async promises docs --- docs/_howto/lolex-async-promises.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_howto/lolex-async-promises.md b/docs/_howto/lolex-async-promises.md index 9f5c84cf5..3bbd56f17 100644 --- a/docs/_howto/lolex-async-promises.md +++ b/docs/_howto/lolex-async-promises.md @@ -62,7 +62,7 @@ it('should return 42 after one second', () => { }); ``` -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 using lolex, so the test passes almost instantly, and not in 1 second. Since `async` functions behave the same way as functions that return promises From beaaff32f389b885bb885a1f37c03713f2b7e34c Mon Sep 17 00:00:00 2001 From: yury Date: Fri, 28 Jun 2019 14:10:34 +0300 Subject: [PATCH 7/7] Explain weird setTimeoutPromise usage in comments --- docs/_howto/lolex-async-promises.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/_howto/lolex-async-promises.md b/docs/_howto/lolex-async-promises.md index 3bbd56f17..56f7c740a 100644 --- a/docs/_howto/lolex-async-promises.md +++ b/docs/_howto/lolex-async-promises.md @@ -42,7 +42,8 @@ The same approach can be used to test an `async` function: ```js module.exports.asyncReturnAfterOneSecond = async () => { - // 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 const setTimeoutPromise = timeout => { return new Promise(resolve => setTimeout(resolve, timeout)); };