mirror of https://github.com/lukechilds/ava.git
James Talmage
9 years ago
committed by
Sindre Sorhus
2 changed files with 132 additions and 0 deletions
@ -0,0 +1,131 @@ |
|||
# When to use `t.plan()` |
|||
|
|||
One major difference between AVA and [`tap`](https://github.com/tapjs/node-tap)/[`tape`](https://github.com/substack/tape) is the behavior of `t.plan()`. In AVA, `t.plan()` is only used to assert that the expected number of assertions are called; it does not auto-end the test. |
|||
|
|||
## Poor uses of `t.plan()` |
|||
|
|||
Many users transitioning from `tap`/`tape` are accustomed to using `t.plan()` prolifically in every test. However, in AVA, we don't consider that to be a "best practice". Instead, we believe `t.plan()` should only be used in situations where it provides some value. |
|||
|
|||
### Sync tests with no branching |
|||
|
|||
`t.plan()` is unnecessary in most sync tests. |
|||
|
|||
```js |
|||
test(t => { |
|||
// BAD: there is no branching here - t.plan() is pointless |
|||
t.plan(2); |
|||
|
|||
t.is(1 + 1, 2); |
|||
t.is(2 + 2, 4); |
|||
}); |
|||
``` |
|||
|
|||
`t.plan()` does not provide any value here, and creates an extra chore if you ever decide to add or remove assertions. |
|||
|
|||
### Promises that are expected to resolve |
|||
|
|||
```js |
|||
test(t => { |
|||
t.plan(1); |
|||
|
|||
return somePromise().then(result => { |
|||
t.is(result, 'foo'); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
At a glance, this tests appears to make good use of `t.plan()` since an async promise handler is involved. However there are several problems with the test: |
|||
|
|||
1. `t.plan()` is presumably used here to protect against the possibility that `somePromise()` might be rejected; But returning a rejected promise would fail the test anyways. |
|||
|
|||
2. It would be better to take advantage of `async`/`await`: |
|||
|
|||
```js |
|||
test(async t => { |
|||
t.is(await somePromise(), 'foo'); |
|||
}); |
|||
``` |
|||
|
|||
## Good uses of `t.plan()` |
|||
|
|||
`t.plan()` has many acceptable uses. |
|||
|
|||
### Promises with a `.catch()` block |
|||
|
|||
```js |
|||
test(t => { |
|||
t.plan(2); |
|||
|
|||
return shouldRejectWithFoo().catch(reason => { |
|||
t.is(reason.message, 'Hello') // Prefer t.throws() if all you care about is the message |
|||
t.is(reason.foo, 'bar'); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
Here, `t.plan()` is used to ensure the code inside the `catch` block happens. In most cases, you should prefer the `t.throws()` assertion, but this is an acceptable use since `t.throws()` only allows you to assert against the error's `message` property. |
|||
|
|||
### Ensuring a catch statement happens |
|||
|
|||
```js |
|||
test(t => { |
|||
t.plan(2); |
|||
|
|||
try { |
|||
shouldThrow(); |
|||
} catch (err) { |
|||
t.is(err.message, 'Hello') // Prefer t.throws() if all you care about is the message |
|||
t.is(err.foo, 'bar'); |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
As stated in the `try`/`catch` example above, using the `t.throws()` assertion is usually a better choice, but it only lets you assert against the error's `message` property. |
|||
|
|||
### Ensuring multiple callbacks are actually called |
|||
|
|||
```js |
|||
test.cb(t => { |
|||
t.plan(2); |
|||
|
|||
const callbackA = () => { |
|||
t.pass(); |
|||
t.end(); |
|||
}; |
|||
|
|||
const callbackB = () => t.pass(); |
|||
|
|||
bThenA(callbackA, callbackB); |
|||
}); |
|||
``` |
|||
|
|||
The above ensures `callbackB` is called first (and only once), followed by `callbackA`. Any other combination would not satisfy the plan. |
|||
|
|||
### Tests with branching statements |
|||
|
|||
In most cases, it's a bad idea to use any complex branching inside your tests. A notable exception is for tests that are auto-generated (perhaps from a JSON document). Below `t.plan()` is used to ensure the correctness of the JSON input: |
|||
|
|||
```js |
|||
const testData = require('./fixtures/test-definitions.json'); |
|||
|
|||
testData.forEach(testDefinition => { |
|||
test(t => { |
|||
const result = functionUnderTest(testDefinition.input); |
|||
|
|||
// testDefinition should have an expectation for `foo` or `bar` but not both |
|||
t.plan(1); |
|||
|
|||
if (testDefinition.foo) { |
|||
t.is(result.foo, testDefinition.foo); |
|||
} |
|||
|
|||
if (testDefinition.bar) { |
|||
t.is(result.bar, testDefinition.foo); |
|||
} |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
## Conclusion |
|||
|
|||
`t.plan()` has plenty of valid uses, but it should not be used indiscriminately. A good rule of thumb is to use it any time your *test* does not have straightforward, easily reasoned about, code flow. Tests with assertions inside callbacks, `if`/`then` statements, `for`/`while` loops, and (in some cases) `try`/`catch` blocks, are all good candidates for `t.plan()`. |
Loading…
Reference in new issue