Skip to content

Commit

Permalink
Close #492 PR: recipe: when to use t.plan.
Browse files Browse the repository at this point in the history
  • Loading branch information
jamestalmage authored and sindresorhus committed Feb 1, 2016
1 parent c22eabd commit 39982a5
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 0 deletions.
131 changes: 131 additions & 0 deletions docs/recipes/when-to-use-plan.md
Original file line number Diff line number Diff line change
@@ -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()`.
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,7 @@ Concurrency is not parallelism. It enables parallelism. [Learn more.](http://sta

- [Code coverage](docs/recipes/code-coverage.md)
- [Endpoint testing](docs/recipes/endpoint-testing.md)
- [When to use `t.plan()`](docs/recipes/when-to-use-plan.md)


## Support
Expand Down

0 comments on commit 39982a5

Please sign in to comment.