Skip to content

Commit 3ed1f4d

Browse files
authored
Merge pull request #5 from MarcL/day4
Day 4 : Time Travel with Fake Timers
2 parents 503edbe + b22abd6 commit 3ed1f4d

File tree

5 files changed

+288
-6
lines changed

5 files changed

+288
-6
lines changed

doc/day4.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# Day 4 notes - Time travel with fake timers
2+
3+
## Speed up yesterday's asynchronous tests
4+
5+
### Copy day 3 source code and first test
6+
7+
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
8+
9+
```javascript
10+
function timeout(callback) {
11+
setTimeout(() => callback('hello'), 1000);
12+
}
13+
14+
export {
15+
timeout
16+
};
17+
```
18+
19+
Impor the function in your test code.
20+
21+
```javascript
22+
import {expect} from 'chai';
23+
import {timeout} from '../src/day4';
24+
25+
describe('day 4 tests', () => {
26+
it('should return expected value from callback', (done) => {
27+
timeout((returnedData) => {
28+
expect(returnedData).to.equal('hello');
29+
done();
30+
});
31+
});
32+
});
33+
```
34+
35+
If you run the code you'll see that it still runs in just over 1000ms.
36+
37+
```shell
38+
mocha --require babel-register test/day4.test.js
39+
```
40+
41+
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.
42+
43+
```shell
44+
npm install --save-dev sinon
45+
```
46+
47+
You can now update the test to use the fake timer.
48+
49+
```javascript
50+
describe('timeout tests', () => {
51+
let clock;
52+
beforeEach(() => {
53+
// Set the fake timer before all of our tests
54+
clock = sinon.useFakeTimers();
55+
});
56+
57+
afterEach(() => {
58+
// Restore back to async time/date functions after each test
59+
clock.restore();
60+
});
61+
62+
it('should return expected value from callback', (done) => {
63+
timeout((returnedData) => {
64+
expect(returnedData).to.equal('hello');
65+
done();
66+
});
67+
68+
// You need to tick the clock by the amount you need.
69+
// Here it's 1000ms or 1 second to allow the timeout callback to trigger
70+
clock.tick(1000);
71+
});
72+
});
73+
```
74+
75+
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.
76+
77+
## DateDescriber - faking dates
78+
79+
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.
80+
81+
```javascript
82+
// Under timeout code
83+
84+
function dateDescriber(otherDate) {
85+
}
86+
87+
export {
88+
timeout,
89+
dateDescriber
90+
};
91+
```
92+
93+
Create a new test `describe` block and add a test to check that the date is in the future.
94+
95+
```javascript
96+
describe('dateDescriber tests', () => {
97+
it('should correctly describe a future year', () => {
98+
const description = dateDescriber(
99+
new Date(2018, 1, 1)
100+
);
101+
102+
expect(description).to.equal('in the future');
103+
});
104+
105+
it('should correctly describe a past year', () => {
106+
const description = dateDescriber(
107+
new Date(2016, 1, 1)
108+
);
109+
110+
expect(description).to.equal('in the past');
111+
});
112+
113+
it('should correctly describe current year', () => {
114+
const description = dateDescriber(
115+
new Date(2017, 1, 1)
116+
);
117+
118+
expect(description).to.equal('this year');
119+
});
120+
});
121+
```
122+
123+
If you run the tests, they will fail so let's add the code to make them all pass:
124+
125+
```javascript
126+
function dateDescriber(otherDate) {
127+
const dateNow = new Date();
128+
129+
const otherYear = otherDate.getFullYear();
130+
const currentYear = dateNow.getFullYear();
131+
132+
const yearDifference = otherYear - currentYear;
133+
134+
if (yearDifference > 0) {
135+
return 'in the future';
136+
} else if (yearDifference < 0) {
137+
return 'in the past';
138+
} else {
139+
return 'this year';
140+
}
141+
}
142+
```
143+
144+
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.
145+
146+
```javascript
147+
describe('dateDescriber tests', () => {
148+
let clock;
149+
const currentYear = 2017;
150+
151+
beforeEach(() => {
152+
const now = new Date(currentYear, 1, 1);
153+
clock = sinon.useFakeTimers(now);
154+
});
155+
156+
afterEach(() => {
157+
clock.restore();
158+
});
159+
160+
// Other code snipped
161+
```
162+
163+
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.
164+
165+
```javascript
166+
it('should correctly describe a future year', () => {
167+
const description = dateDescriber(
168+
new Date(currentYear + 1, 1, 1)
169+
);
170+
171+
expect(description).to.equal('in the future');
172+
});
173+
174+
it('should correctly describe a past year', () => {
175+
const description = dateDescriber(
176+
new Date(currentYear - 1, 1, 1)
177+
);
178+
179+
expect(description).to.equal('in the past');
180+
});
181+
182+
it('should correctly describe current year', () => {
183+
const description = dateDescriber(
184+
new Date(currentYear, 1, 1)
185+
);
186+
187+
expect(description).to.equal('this year');
188+
});
189+
```
190+
191+
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!

doc/notes.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@
2424

2525
## Day 4 - Time travel with fake timers
2626

27-
1. Install sinon
28-
2. Fake timer in test
29-
3. Use mocha to stub and restore timers
30-
4. Date code which displays different strings based on date
31-
5. Set fake date to now and tick to the future
27+
1. Speed up slow tests
28+
2. Use a fake timer
29+
3. Use sinon to stub and restore timers
30+
4. mocha beforeEach / afterEach
31+
5. Date describer - tell us if the date is in the future
32+
6. Fake the current time
3233

3334
## Day 5 - GitHub stargazing with stubs & spies
3435

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"babel-preset-env": "~1.6.0",
2222
"babel-register": "~6.24.1",
2323
"chai": "~4.1.1",
24-
"mocha": "~3.5.0"
24+
"mocha": "~3.5.0",
25+
"sinon": "~3.2.1"
2526
}
2627
}

src/day4.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
function timeout(callback) {
2+
setTimeout(() => callback('hello'), 1000);
3+
}
4+
5+
function dateDescriber(otherDate) {
6+
const dateNow = new Date();
7+
8+
const otherYear = otherDate.getFullYear();
9+
const currentYear = dateNow.getFullYear();
10+
11+
const yearDifference = otherYear - currentYear;
12+
13+
if (yearDifference > 0) {
14+
return 'in the future';
15+
} else if (yearDifference < 0) {
16+
return 'in the past';
17+
} else {
18+
return 'this year';
19+
}
20+
}
21+
22+
export {
23+
timeout,
24+
dateDescriber
25+
};
26+

test/day4.test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import sinon from 'sinon';
2+
import {expect} from 'chai';
3+
import {timeout, dateDescriber} from '../src/day4';
4+
5+
describe('day 4 tests', () => {
6+
describe('timeout tests', () => {
7+
let clock;
8+
beforeEach(() => {
9+
clock = sinon.useFakeTimers();
10+
});
11+
12+
afterEach(() => {
13+
clock.restore();
14+
});
15+
16+
it('should return expected value from callback', (done) => {
17+
timeout((returnedData) => {
18+
expect(returnedData).to.equal('hello');
19+
done();
20+
});
21+
22+
clock.tick(1000);
23+
});
24+
});
25+
26+
describe('dateDescriber tests', () => {
27+
let clock;
28+
const currentYear = 2017;
29+
30+
beforeEach(() => {
31+
const now = new Date(currentYear, 1, 1);
32+
clock = sinon.useFakeTimers(now);
33+
});
34+
35+
afterEach(() => {
36+
clock.restore();
37+
});
38+
39+
it('should correctly describe a future year', () => {
40+
const description = dateDescriber(
41+
new Date(currentYear + 1, 1, 1)
42+
);
43+
44+
expect(description).to.equal('in the future');
45+
});
46+
47+
it('should correctly describe a past year', () => {
48+
const description = dateDescriber(
49+
new Date(currentYear - 1, 1, 1)
50+
);
51+
52+
expect(description).to.equal('in the past');
53+
});
54+
55+
it('should correctly describe current year', () => {
56+
const description = dateDescriber(
57+
new Date(currentYear, 1, 1)
58+
);
59+
60+
expect(description).to.equal('this year');
61+
});
62+
});
63+
});

0 commit comments

Comments
 (0)