Skip to content

Commit

Permalink
recipe: when to use t.plan
Browse files Browse the repository at this point in the history
  • Loading branch information
jamestalmage committed Jan 30, 2016
1 parent 135e53a commit 5e4e0bf
Showing 1 changed file with 124 additions and 0 deletions.
124 changes: 124 additions & 0 deletions docs/recipes/when-to-use-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Using t.plan in AVA

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(function (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 errors `message` property.

### Ensuring a catch statement happens

```js
test(t => {
t.plan(1);
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 let's you assert against the errors `message` property.

### Ensuring multiple callbacks are actually called

```js
test.cb(t => {
t.plan(2);
const callbackA = () => {
t.pass();
t.end();
};
const callbackA = () => t.pass();
bThenA(a, b);
});
```

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 is 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
var testData = require('./fixtures/test-definitions.json');

testData.forEach(function (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);
}
});
});
```

It is worth mentioning here that `t.plan` does incur a (very, very) small performance penalty. AVA stores the stack trace each time `t.plan` is called, and that generally takes a few microseconds. This should **NOT** discourage you from using it where appropriate. You would need to call `t.plan` thousands of times to notice a difference of even a fraction of a second.

## 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 `if`/`then` statements, `for`/`while` loops, and (in some cases) `try`/`catch` blocks, are all good candidates for `t.plan`.

0 comments on commit 5e4e0bf

Please sign in to comment.