From 02da15b879f4edcd5cb1a3d08a6ab5dfeb73c274 Mon Sep 17 00:00:00 2001 From: Marc Littlemore Date: Sun, 27 Aug 2017 18:57:08 +0100 Subject: [PATCH 1/2] Day 4 code --- doc/day4.md | 136 ++++++++++++++++++++++++++++++++++++++++++++++ doc/notes.md | 25 +++++++-- package.json | 3 +- src/day4.js | 26 +++++++++ test/day4.test.js | 64 ++++++++++++++++++++++ 5 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 doc/day4.md create mode 100644 src/day4.js create mode 100644 test/day4.test.js diff --git a/doc/day4.md b/doc/day4.md new file mode 100644 index 0000000..03d48e3 --- /dev/null +++ b/doc/day4.md @@ -0,0 +1,136 @@ +# Day 4 notes - Time travel with fake timers + +### Copy day 3 source code and first test + +```javascript +function timeout(callback) { + setTimeout(() => callback('hello'), 1000); +} + +export { + timeout +}; +``` + +```javascript +import {expect} from 'chai'; +import {timeout} from '../src/day4'; + +describe('day 4 tests', () => { + it('should return expected value from callback', (done) => { + const clock = sinon.useFakeTimers(); + + timeout((returnedData) => { + expect(returnedData).to.equal('hello'); + done(); + }); + + clock.tick(1000); + }); +}); +``` + +* Now runs really quick ~100ms + +## New test with dates + +```javascript +function dateDescriber(date) { + const dateNow = Date.now(); + console.log(dateNow); +} +``` + +* Runs slowly +* Want unit tests run quickly - if we have hundreds of tests then take a long time +* So we can cheat and time travel +* Use sinon + +## Install sinon + +```shell +npm install --save-dev sinon +``` + +## Add timeout test + +```javascript +it('should return expected value from callback', () => { + day3((returnedData) => { + expect(returnedData).to.equal('hello'); + }); +}); +``` +* Run the tests - they appear to pass +* We've got no code yet! +* Let's write the code + +```javascript +function day3(callback) { + setTimeout(() => { + callback('hello'); + }, 1000); +} +``` + +* Run the tests - they still appear to pass +* It thinks test is synchronous +* Need to tell Mocha it's an asynchronous +* Add done to callback function and call it when we've done + +```javascript +it('should return expected value from callback', (done) => { + day3((returnedData) => { + expect(returnedData).to.equal('hello'); + done(); + }); +}); +``` + +## Promises + +```javascript +it('should return expected value from promise', () => { + day3() + .then((returnedData) => { + expect(returnedData).to.equal('hello'); + }); +}); +``` + +```javascript +function day3(callback) { + if (callback) { + setTimeout(() => { + callback('hello'); + }, 1000); + } else { + return Promise.resolve('hello'); + } +} +``` + +* Test to return a promise which does the same +* Gotcha : Not using done - tests pass +* Use `done` again + +```javascript +it('should return expected value from promise', (done) => { + day3() + .then((returnedData) => { + expect(returnedData).to.equal('hello'); + done(); + }); +}); +``` + +* Better way - return a promise + +```javascript +it('should return expected value from promise', () => { + return day3() + .then((returnedData) => { + expect(returnedData).to.equal('hello'); + }); +}); +``` diff --git a/doc/notes.md b/doc/notes.md index 0b41863..d81a10c 100644 --- a/doc/notes.md +++ b/doc/notes.md @@ -24,11 +24,26 @@ ## Day 4 - Time travel with fake timers -1. Install sinon -2. Fake timer in test -3. Use mocha to stub and restore timers -4. Date code which displays different strings based on date -5. Set fake date to now and tick to the future +1. Speed up slow tests +2. Use a fake timer +3. Use sinon to stub and restore timers +4. mocha beforeEach / afterEach +5. Date describer - tell us if the date is in the future +6. Fake the current time + + + + + + + + + + + + + + ## Day 5 - GitHub stargazing with stubs & spies diff --git a/package.json b/package.json index a11bcf5..31eff9b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "babel-preset-env": "~1.6.0", "babel-register": "~6.24.1", "chai": "~4.1.1", - "mocha": "~3.5.0" + "mocha": "~3.5.0", + "sinon": "~3.2.1" } } diff --git a/src/day4.js b/src/day4.js new file mode 100644 index 0000000..96020d5 --- /dev/null +++ b/src/day4.js @@ -0,0 +1,26 @@ +function timeout(callback) { + setTimeout(() => callback('hello'), 1000); +} + +function dateDescriber(otherDate) { + const dateNow = new Date(); + + const otherYear = otherDate.getFullYear(); + const currentYear = dateNow.getFullYear(); + + const yearDifference = otherYear - currentYear; + + if (yearDifference > 0) { + return 'in the future'; + } else if (yearDifference < 0) { + return 'in the past'; + } else { + return 'this year'; + } +} + +export { + timeout, + dateDescriber +}; + diff --git a/test/day4.test.js b/test/day4.test.js new file mode 100644 index 0000000..5e7264b --- /dev/null +++ b/test/day4.test.js @@ -0,0 +1,64 @@ +import sinon from 'sinon'; +import {expect} from 'chai'; +import {timeout, dateDescriber} from '../src/day4'; + +describe('day 4 tests', () => { + describe('timeout tests', () => { + let clock; + beforeEach(() => { + clock = sinon.useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('should return expected value from callback', (done) => { + + timeout((returnedData) => { + expect(returnedData).to.equal('hello'); + done(); + }); + + clock.tick(1000); + }); + }); + + describe('dateDescriber tests', () => { + let clock; + const currentYear = 2017; + + beforeEach(() => { + const now = new Date(currentYear, 1, 1); + clock = sinon.useFakeTimers(now); + }); + + afterEach(() => { + clock.restore(); + }); + + it('should correctly describe a future year', () => { + const description = dateDescriber( + new Date(currentYear + 1, 1, 1) + ); + + expect(description).to.equal('in the future'); + }); + + it('should correctly describe a past year', () => { + const description = dateDescriber( + new Date(currentYear - 1, 1, 1) + ); + + expect(description).to.equal('in the past'); + }); + + it('should correctly describe current year', () => { + const description = dateDescriber( + new Date(currentYear, 1, 1) + ); + + expect(description).to.equal('this year'); + }); + }); +}); From b22abd6922fa6339dacc834033d290b6cc2c5e78 Mon Sep 17 00:00:00 2001 From: Marc Littlemore Date: Sun, 27 Aug 2017 21:54:32 +0100 Subject: [PATCH 2/2] Update notes for day4 --- doc/day4.md | 191 +++++++++++++++++++++++++++++----------------- doc/notes.md | 14 ---- test/day4.test.js | 1 - 3 files changed, 123 insertions(+), 83 deletions(-) diff --git a/doc/day4.md b/doc/day4.md index 03d48e3..d40a591 100644 --- a/doc/day4.md +++ b/doc/day4.md @@ -1,7 +1,11 @@ # Day 4 notes - Time travel with fake timers +## Speed up yesterday's asynchronous tests + ### Copy day 3 source code and first test +Make a copy of the code from day3 but we only testing our slow `setTimeout` code and not the promise. **Note:** don't export as a default ES6 module as we're going to export a couple of functions + ```javascript function timeout(callback) { setTimeout(() => callback('hello'), 1000); @@ -12,125 +16,176 @@ export { }; ``` +Impor the function in your test code. + ```javascript import {expect} from 'chai'; import {timeout} from '../src/day4'; describe('day 4 tests', () => { it('should return expected value from callback', (done) => { - const clock = sinon.useFakeTimers(); - timeout((returnedData) => { expect(returnedData).to.equal('hello'); done(); }); - - clock.tick(1000); }); }); ``` -* Now runs really quick ~100ms +If you run the code you'll see that it still runs in just over 1000ms. -## New test with dates - -```javascript -function dateDescriber(date) { - const dateNow = Date.now(); - console.log(dateNow); -} +```shell +mocha --require babel-register test/day4.test.js ``` -* Runs slowly -* Want unit tests run quickly - if we have hundreds of tests then take a long time -* So we can cheat and time travel -* Use sinon - -## Install sinon +You're going to install `sinon` which is a library of test double functions. It has a special fake timer implementation which makes asynchronous time-related functions become syncronous and controllable. ```shell npm install --save-dev sinon ``` -## Add timeout test +You can now update the test to use the fake timer. ```javascript -it('should return expected value from callback', () => { - day3((returnedData) => { - expect(returnedData).to.equal('hello'); +describe('timeout tests', () => { + let clock; + beforeEach(() => { + // Set the fake timer before all of our tests + clock = sinon.useFakeTimers(); + }); + + afterEach(() => { + // Restore back to async time/date functions after each test + clock.restore(); + }); + + it('should return expected value from callback', (done) => { + timeout((returnedData) => { + expect(returnedData).to.equal('hello'); + done(); + }); + + // You need to tick the clock by the amount you need. + // Here it's 1000ms or 1 second to allow the timeout callback to trigger + clock.tick(1000); }); }); ``` -* Run the tests - they appear to pass -* We've got no code yet! -* Let's write the code + +Try the test again and you'll see that it runs in about 150ms! A great improvement and the sort of speed you want from unit tests. + +## DateDescriber - faking dates + +Let's add a new function which will take a date and tell us whether the year is in the future, the past or the current year. ```javascript -function day3(callback) { - setTimeout(() => { - callback('hello'); - }, 1000); +// Under timeout code + +function dateDescriber(otherDate) { } + +export { + timeout, + dateDescriber +}; ``` -* Run the tests - they still appear to pass -* It thinks test is synchronous -* Need to tell Mocha it's an asynchronous -* Add done to callback function and call it when we've done +Create a new test `describe` block and add a test to check that the date is in the future. ```javascript -it('should return expected value from callback', (done) => { - day3((returnedData) => { - expect(returnedData).to.equal('hello'); - done(); +describe('dateDescriber tests', () => { + it('should correctly describe a future year', () => { + const description = dateDescriber( + new Date(2018, 1, 1) + ); + + expect(description).to.equal('in the future'); }); -}); -``` -## Promises + it('should correctly describe a past year', () => { + const description = dateDescriber( + new Date(2016, 1, 1) + ); -```javascript -it('should return expected value from promise', () => { - day3() - .then((returnedData) => { - expect(returnedData).to.equal('hello'); - }); + expect(description).to.equal('in the past'); + }); + + it('should correctly describe current year', () => { + const description = dateDescriber( + new Date(2017, 1, 1) + ); + + expect(description).to.equal('this year'); + }); }); ``` +If you run the tests, they will fail so let's add the code to make them all pass: + ```javascript -function day3(callback) { - if (callback) { - setTimeout(() => { - callback('hello'); - }, 1000); +function dateDescriber(otherDate) { + const dateNow = new Date(); + + const otherYear = otherDate.getFullYear(); + const currentYear = dateNow.getFullYear(); + + const yearDifference = otherYear - currentYear; + + if (yearDifference > 0) { + return 'in the future'; + } else if (yearDifference < 0) { + return 'in the past'; } else { - return Promise.resolve('hello'); + return 'this year'; } } ``` -* Test to return a promise which does the same -* Gotcha : Not using done - tests pass -* Use `done` again +Now all of the tests will pass but we have a major problem. These tests will run correctly but what happens if it's 2018? They'll start to fail. What should we do? Let's use `sinon.useFakeTimer` to set the initial date to what we would like it to be. Add a `beforeEach` and `afterEach` function to the `describe` block and let's set the initial date to January 1 2017. ```javascript -it('should return expected value from promise', (done) => { - day3() - .then((returnedData) => { - expect(returnedData).to.equal('hello'); - done(); - }); -}); +describe('dateDescriber tests', () => { + let clock; + const currentYear = 2017; + + beforeEach(() => { + const now = new Date(currentYear, 1, 1); + clock = sinon.useFakeTimers(now); + }); + + afterEach(() => { + clock.restore(); + }); + + // Other code snipped ``` -* Better way - return a promise +Great! The tests now pass and we've set the initial date. However, notice that we're still hardcoding dates into our tests. If we change `currentYear` to 2018, our future test will still fail. It would be better to make them relative to our `currentYear` variable. Let's tidy up our tests so we're know they're always correct. ```javascript -it('should return expected value from promise', () => { - return day3() - .then((returnedData) => { - expect(returnedData).to.equal('hello'); - }); +it('should correctly describe a future year', () => { + const description = dateDescriber( + new Date(currentYear + 1, 1, 1) + ); + + expect(description).to.equal('in the future'); +}); + +it('should correctly describe a past year', () => { + const description = dateDescriber( + new Date(currentYear - 1, 1, 1) + ); + + expect(description).to.equal('in the past'); +}); + +it('should correctly describe current year', () => { + const description = dateDescriber( + new Date(currentYear, 1, 1) + ); + + expect(description).to.equal('this year'); }); ``` + +Now you should be happy with your tests. They all pass and the dates won't alter if you run them on a different environment or in the future. Great work! \ No newline at end of file diff --git a/doc/notes.md b/doc/notes.md index d81a10c..8354626 100644 --- a/doc/notes.md +++ b/doc/notes.md @@ -31,20 +31,6 @@ 5. Date describer - tell us if the date is in the future 6. Fake the current time - - - - - - - - - - - - - - ## Day 5 - GitHub stargazing with stubs & spies ## Day 6 - Simplify & automate your tests diff --git a/test/day4.test.js b/test/day4.test.js index 5e7264b..a955263 100644 --- a/test/day4.test.js +++ b/test/day4.test.js @@ -14,7 +14,6 @@ describe('day 4 tests', () => { }); it('should return expected value from callback', (done) => { - timeout((returnedData) => { expect(returnedData).to.equal('hello'); done();