Skip to content

Commit ae50a13

Browse files
jasonritchienovemberborn
authored andcommitted
Add recipe on how to set up tests
1 parent 98e66da commit ae50a13

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

docs/recipes/test-setup.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Test setup
2+
3+
Tests can be set up using the `beforeEach()` hook. Often though you could use a plain setup function instead. This recipe helps you decide what's best for your use case.
4+
5+
# The `beforeEach()` hook versus setup functions
6+
7+
The `beforeEach()` hook has some downsides. For example, you cannot turn it off for specific tests, nor can you apply it to specific tests. As an alternative, you can use simple functions. This allows you to use multiple setup functions for different setup requirements and call different parts of setup from different tests. You can even have setup functions with parameters so tests can customize their own setup.
8+
9+
Let's say you have a function that interacts with the file system. Perhaps you run a few tests using `mock-fs`, and then a few that use the real file system and a temporary directory. Or you have a setup function that you run with valid data for some tests and invalid data for other tests, all within the same test file.
10+
11+
You could do all these things using plain setup functions, but there are tradeoffs:
12+
13+
|`beforeEach()`| Setup functions
14+
|---|---
15+
| ⛔️   used for all tests| ✅   can change or skip depending on test
16+
| ⛔️   more overhead for beginners, "some magic"| ✅   easier for beginners, "no magic"
17+
| ✅   supports callback mode, built-in support for observables| ⛔️   must use promises for asynchronous behavior
18+
| ✅   failure has friendly output| ⛔️   errors are attributed to the test
19+
| ✅   corresponding `afterEach` and `afterEach.always` for cleanup| ⛔️   cannot easily clean up
20+
21+
## Complex test setup
22+
23+
In this example, we have both a `beforeEach()` hook, and then more modifications within each test.
24+
25+
```js
26+
test.beforeEach(t => {
27+
setupConditionA(t);
28+
setupConditionB(t);
29+
setupConditionC(t);
30+
});
31+
32+
test('first scenario', t => {
33+
tweakSomething(t);
34+
const someCondition = t.context.thingUnderTest();
35+
t.true(someCondition);
36+
});
37+
38+
test('second scenario', t => {
39+
tweakSomethingElse(t);
40+
const someOtherCondition = t.context.thingUnderTest();
41+
t.true(someOtherCondition);
42+
});
43+
```
44+
45+
If too many variables need changing for each test, consider omitting the `beforeEach()` hook and performing setup steps within the tests themselves.
46+
47+
```js
48+
test('first scenario', t => {
49+
setupConditionA(t);
50+
setupConditionB(t, {/* options */});
51+
setupConditionC(t);
52+
const someCondition = t.context.thingUnderTest();
53+
t.true(someCondition);
54+
});
55+
56+
// In this test, setupConditionB() is never called.
57+
test('second scenario', t => {
58+
setupConditionA(t);
59+
setupConditionC(t);
60+
const someOtherCondition = t.context.thingUnderTest();
61+
t.true(someOtherCondition);
62+
});
63+
```
64+
65+
## A practical example
66+
67+
```js
68+
test.beforeEach(t => {
69+
t.context = {
70+
authenticator: new Authenticator(),
71+
credentials: new Credentials('admin', 's3cr3t')
72+
};
73+
});
74+
75+
test('authenticating with valid credentials', async t => {
76+
const isValid = t.context.authenticator.authenticate(t.context.credentials);
77+
t.true(await isValid);
78+
});
79+
80+
test('authenticating with an invalid username', async t => {
81+
t.context.credentials.username = 'bad_username';
82+
const isValid = t.context.authenticator.authenticate(t.context.credentials);
83+
t.false(await isValid);
84+
});
85+
86+
test('authenticating with an invalid password', async t => {
87+
t.context.credentials.password = 'bad_password';
88+
const isValid = t.context.authenticator.authenticate(t.context.credentials);
89+
t.false(await isValid);
90+
});
91+
```
92+
93+
The same tests, now using setup functions, would look like the following.
94+
95+
```js
96+
function setup({username = 'admin', password = 's3cr3t'} = {}) {
97+
return {
98+
authenticator: new Authenticator(),
99+
credentials: new Credentials(username, password)
100+
};
101+
}
102+
103+
test('authenticating with valid credentials', async t => {
104+
const {authenticator, credentials} = setup();
105+
const isValid = authenticator.authenticate(credentials);
106+
t.true(await isValid);
107+
});
108+
109+
test('authenticating with an invalid username', async t => {
110+
const {authenticator, credentials} = setup({username: 'bad_username'});
111+
const isValid = authenticator.authenticate(credentials);
112+
t.false(await isValid);
113+
});
114+
115+
test('authenticating with an invalid password', async t => {
116+
const {authenticator, credentials} = setup({password: 'bad_password'});
117+
const isValid = authenticator.authenticate(credentials);
118+
t.false(await isValid);
119+
});
120+
```
121+
122+
## Combining hooks and setup functions
123+
124+
Of course `beforeEach()` and plain setup functions can be used together:
125+
126+
```js
127+
test.beforeEach(t => {
128+
t.context = setupAllTests();
129+
});
130+
131+
test('first scenario', t => {
132+
firstSetup(t);
133+
const someCondition = t.context.thingUnderTest();
134+
t.true(someCondition);
135+
});
136+
```

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,7 @@ It's the [Andromeda galaxy](https://simple.wikipedia.org/wiki/Andromeda_galaxy).
11511151

11521152
## Recipes
11531153

1154+
- [Test setup](docs/recipes/test-setup.md)
11541155
- [Code coverage](docs/recipes/code-coverage.md)
11551156
- [Watch mode](docs/recipes/watch-mode.md)
11561157
- [Endpoint testing](docs/recipes/endpoint-testing.md)

0 commit comments

Comments
 (0)