Add t.teardown()
Co-Authored-By: Mark Wubben <mark@novemberborn.net>
This commit is contained in:
parent
993bb9039d
commit
75cbc3b2a5
8 changed files with 218 additions and 4 deletions
|
|
@ -167,6 +167,8 @@ AVA lets you register hooks that are run before and after your tests. This allow
|
|||
|
||||
If a test is skipped with the `.skip` modifier, the respective `.beforeEach()`, `.afterEach()` and `.afterEach.always()` hooks are not run. Likewise, if all tests in a test file are skipped `.before()`, `.after()` and `.after.always()` hooks for the file are not run.
|
||||
|
||||
*You may not need to use `.afterEach.always()` hooks to clean up after a test.* You can use [`t.teardown()`](./02-execution-context.md#tteardownfn) to undo side-effects *within* a particular test.
|
||||
|
||||
Like `test()` these methods take an optional title and an implementation function. The title is shown if your hook fails to execute. The implementation is called with an [execution object](./02-execution-context.md). You can use assertions in your hooks. You can also pass a [macro function](#reusing-test-logic-through-macros) and additional arguments.
|
||||
|
||||
`.before()` hooks execute before `.beforeEach()` hooks. `.afterEach()` hooks execute before `.after()` hooks. Within their category the hooks execute in the order they were defined. By default hooks execute concurrently, but you can use `test.serial` to ensure only that single hook is run at a time. Unlike with tests, serial hooks are *not* run before other hooks:
|
||||
|
|
|
|||
|
|
@ -26,10 +26,6 @@ Contains shared state from hooks.
|
|||
|
||||
Whether a test has passed. This value is only accurate in the `test.afterEach()` and `test.afterEach.always()` hooks.
|
||||
|
||||
## `t.plan(count)`
|
||||
|
||||
Plan how many assertion there are in the test. The test will fail if the actual assertion count doesn't match the number of planned assertions. See [assertion planning](./03-assertions.md#assertion-planning).
|
||||
|
||||
## `t.end()`
|
||||
|
||||
End the test. Only works with `test.cb()`.
|
||||
|
|
@ -38,6 +34,18 @@ End the test. Only works with `test.cb()`.
|
|||
|
||||
Log values contextually alongside the test result instead of immediately printing them to `stdout`. Behaves somewhat like `console.log`, but without support for placeholder tokens.
|
||||
|
||||
## `t.plan(count)`
|
||||
|
||||
Plan how many assertion there are in the test. The test will fail if the actual assertion count doesn't match the number of planned assertions. See [assertion planning](./03-assertions.md#assertion-planning).
|
||||
|
||||
## `t.teardown(fn)`
|
||||
|
||||
Registers the `fn` function to be run after the test has finished. You can register multiple functions and they'll run in order. You can use asynchronous functions: only one will run at a time.
|
||||
|
||||
You cannot perform assertions using the `t` object or register additional functions from inside `fn`.
|
||||
|
||||
You cannot use `t.teardown()` in hooks either.
|
||||
|
||||
## `t.timeout(ms)`
|
||||
|
||||
Set a timeout for the test, in milliseconds. The test will fail if this timeout is exceeded. The timeout is reset each time an assertion is made.
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ test('second scenario', t => {
|
|||
});
|
||||
```
|
||||
|
||||
You can use [`t.teardown()`](../02-execution-context.md#tteardownfn) to register a teardown function which will run after the test has finished (regardless of whether it's passed or failed).
|
||||
|
||||
## A practical example
|
||||
|
||||
```js
|
||||
|
|
|
|||
6
index.d.ts
vendored
6
index.d.ts
vendored
|
|
@ -313,6 +313,7 @@ export interface ExecutionContext<Context = unknown> extends Assertions {
|
|||
|
||||
log: LogFn;
|
||||
plan: PlanFn;
|
||||
teardown: TeardownFn;
|
||||
timeout: TimeoutFn;
|
||||
try: TryFn<Context>;
|
||||
}
|
||||
|
|
@ -344,6 +345,11 @@ export interface TimeoutFn {
|
|||
(ms: number): void;
|
||||
}
|
||||
|
||||
export interface TeardownFn {
|
||||
/** Declare a function to be run after the test has ended. */
|
||||
(fn: () => void): void;
|
||||
}
|
||||
|
||||
export interface TryFn<Context = unknown> {
|
||||
/**
|
||||
* Attempt to run some assertions. The result must be explicitly committed or discarded or else
|
||||
|
|
|
|||
|
|
@ -283,6 +283,7 @@ class Runner extends Emittery {
|
|||
metadata: task.metadata,
|
||||
powerAssert: this.powerAssert,
|
||||
title: `${task.title}${titleSuffix || ''}`,
|
||||
isHook: true,
|
||||
testPassed
|
||||
}));
|
||||
const outcome = await this.runMultiple(hooks, this.serial);
|
||||
|
|
|
|||
35
lib/test.js
35
lib/test.js
|
|
@ -68,6 +68,10 @@ class ExecutionContext extends assert.Assertions {
|
|||
test.timeout(ms);
|
||||
};
|
||||
|
||||
this.teardown = callback => {
|
||||
test.addTeardown(callback);
|
||||
};
|
||||
|
||||
this.try = async (...attemptArgs) => {
|
||||
const {args, buildTitle, implementations, receivedImplementationArray} = parseTestArgs(attemptArgs);
|
||||
|
||||
|
|
@ -193,12 +197,14 @@ class Test {
|
|||
this.experiments = options.experiments || {};
|
||||
this.failWithoutAssertions = options.failWithoutAssertions;
|
||||
this.fn = options.fn;
|
||||
this.isHook = options.isHook === true;
|
||||
this.metadata = options.metadata;
|
||||
this.powerAssert = options.powerAssert;
|
||||
this.title = options.title;
|
||||
this.testPassed = options.testPassed;
|
||||
this.registerUniqueTitle = options.registerUniqueTitle;
|
||||
this.logs = [];
|
||||
this.teardowns = [];
|
||||
|
||||
const {snapshotBelongsTo = this.title, nextSnapshotIndex = 0} = options;
|
||||
this.snapshotBelongsTo = snapshotBelongsTo;
|
||||
|
|
@ -457,6 +463,34 @@ class Test {
|
|||
this.timeoutTimer = null;
|
||||
}
|
||||
|
||||
addTeardown(callback) {
|
||||
if (this.isHook) {
|
||||
this.saveFirstError(new Error('`t.teardown()` is not allowed in hooks'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.finishing) {
|
||||
this.saveFirstError(new Error('`t.teardown()` cannot be used during teardown'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError('Expected a function');
|
||||
}
|
||||
|
||||
this.teardowns.push(callback);
|
||||
}
|
||||
|
||||
async runTeardowns() {
|
||||
for (const teardown of this.teardowns) {
|
||||
try {
|
||||
await teardown(); // eslint-disable-line no-await-in-loop
|
||||
} catch (error) {
|
||||
this.saveFirstError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verifyPlan() {
|
||||
if (!this.assertError && this.planCount !== null && this.planCount !== this.assertCount) {
|
||||
this.saveFirstError(new assert.AssertionError({
|
||||
|
|
@ -670,6 +704,7 @@ class Test {
|
|||
this.clearTimeout();
|
||||
this.verifyPlan();
|
||||
this.verifyAssertions();
|
||||
await this.runTeardowns();
|
||||
|
||||
this.duration = nowAndTimers.now() - this.startedAt;
|
||||
|
||||
|
|
|
|||
|
|
@ -546,3 +546,23 @@ test('shared context of any type', t => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('teardowns cannot be used in hooks', async t => {
|
||||
let hookFailure = null;
|
||||
await promiseEnd(new Runner(), runner => {
|
||||
runner.on('stateChange', evt => {
|
||||
if (evt.type === 'hook-failed') {
|
||||
hookFailure = evt;
|
||||
}
|
||||
});
|
||||
|
||||
runner.chain.beforeEach(a => {
|
||||
a.teardown(() => {});
|
||||
});
|
||||
|
||||
runner.chain('test', a => a.pass());
|
||||
});
|
||||
|
||||
t.ok(hookFailure);
|
||||
t.match(hookFailure.err.message, /not allowed in hooks/);
|
||||
});
|
||||
|
|
|
|||
140
test-tap/test.js
140
test-tap/test.js
|
|
@ -5,6 +5,7 @@ require('../lib/worker/options').set({});
|
|||
const path = require('path');
|
||||
const React = require('react');
|
||||
const {test} = require('tap');
|
||||
const sinon = require('sinon');
|
||||
const delay = require('delay');
|
||||
const snapshotManager = require('../lib/snapshot-manager');
|
||||
const Test = require('../lib/test');
|
||||
|
|
@ -746,6 +747,133 @@ test('timeout is refreshed on assert', t => {
|
|||
});
|
||||
});
|
||||
|
||||
test('teardown passing test', t => {
|
||||
const teardown = sinon.spy();
|
||||
return ava(a => {
|
||||
a.teardown(teardown);
|
||||
a.pass();
|
||||
}).run().then(result => {
|
||||
t.is(result.passed, true);
|
||||
t.ok(teardown.calledOnce);
|
||||
});
|
||||
});
|
||||
|
||||
test('teardown failing test', t => {
|
||||
const teardown = sinon.spy();
|
||||
return ava(a => {
|
||||
a.teardown(teardown);
|
||||
a.fail();
|
||||
}).run().then(result => {
|
||||
t.is(result.passed, false);
|
||||
t.ok(teardown.calledOnce);
|
||||
});
|
||||
});
|
||||
|
||||
test('teardown awaits promise', t => {
|
||||
let tornDown = false;
|
||||
const teardownPromise = delay(200).then(() => {
|
||||
tornDown = true;
|
||||
});
|
||||
return ava(a => {
|
||||
a.teardown(() => teardownPromise);
|
||||
a.pass();
|
||||
}).run().then(result => {
|
||||
t.is(result.passed, true);
|
||||
t.ok(tornDown);
|
||||
});
|
||||
});
|
||||
|
||||
test('teardowns run sequentially in order', t => {
|
||||
const teardownA = sinon.stub().resolves(delay(200));
|
||||
let resolveB;
|
||||
const teardownB = sinon.stub().returns(new Promise(resolve => {
|
||||
resolveB = resolve;
|
||||
}));
|
||||
return ava(a => {
|
||||
a.teardown(() => teardownA().then(resolveB));
|
||||
a.teardown(teardownB);
|
||||
a.pass();
|
||||
}).run().then(result => {
|
||||
t.is(result.passed, true);
|
||||
t.ok(teardownA.calledBefore(teardownB));
|
||||
});
|
||||
});
|
||||
|
||||
test('teardown with cb', t => {
|
||||
const teardown = sinon.spy();
|
||||
return ava.cb(a => {
|
||||
a.teardown(teardown);
|
||||
setTimeout(() => {
|
||||
a.pass();
|
||||
a.end();
|
||||
});
|
||||
}).run().then(result => {
|
||||
t.is(result.passed, true);
|
||||
t.ok(teardown.calledOnce);
|
||||
});
|
||||
});
|
||||
|
||||
test('teardown without function callback fails', t => {
|
||||
return ava(a => {
|
||||
return a.throwsAsync(async () => {
|
||||
a.teardown(false);
|
||||
}, {message: 'Expected a function'});
|
||||
}).run().then(result => {
|
||||
t.is(result.passed, true);
|
||||
});
|
||||
});
|
||||
|
||||
test('teardown errors fail the test', t => {
|
||||
const teardown = sinon.stub().throws('TeardownError');
|
||||
return ava(a => {
|
||||
a.teardown(teardown);
|
||||
a.pass();
|
||||
}).run().then(result => {
|
||||
t.is(result.passed, false);
|
||||
t.is(result.error.name, 'TeardownError');
|
||||
t.ok(teardown.calledOnce);
|
||||
});
|
||||
});
|
||||
|
||||
test('teardown errors are hidden behind assertion errors', t => {
|
||||
const teardown = sinon.stub().throws('TeardownError');
|
||||
return ava(a => {
|
||||
a.teardown(teardown);
|
||||
a.fail();
|
||||
}).run().then(result => {
|
||||
t.is(result.passed, false);
|
||||
t.is(result.error.name, 'AssertionError');
|
||||
t.ok(teardown.calledOnce);
|
||||
});
|
||||
});
|
||||
|
||||
test('teardowns errors do not stop next teardown from running', t => {
|
||||
const teardownA = sinon.stub().throws('TeardownError');
|
||||
const teardownB = sinon.spy();
|
||||
return ava(a => {
|
||||
a.teardown(teardownA);
|
||||
a.teardown(teardownB);
|
||||
a.pass();
|
||||
}).run().then(result => {
|
||||
t.is(result.passed, false);
|
||||
t.is(result.error.name, 'TeardownError');
|
||||
t.ok(teardownA.calledOnce);
|
||||
t.ok(teardownB.calledOnce);
|
||||
t.ok(teardownA.calledBefore(teardownB));
|
||||
});
|
||||
});
|
||||
|
||||
test('teardowns cannot be registered by teardowns', async t => {
|
||||
const result = await ava(a => {
|
||||
a.teardown(() => {
|
||||
a.teardown(() => {});
|
||||
});
|
||||
a.pass();
|
||||
}).run();
|
||||
t.is(result.passed, false);
|
||||
t.match(result.error.message, /cannot be used during teardown/);
|
||||
});
|
||||
|
||||
test('.log() is bound', t => {
|
||||
return ava(a => {
|
||||
const {log} = a;
|
||||
|
|
@ -796,3 +924,15 @@ test('.end() is bound', t => {
|
|||
t.true(result.passed);
|
||||
});
|
||||
});
|
||||
|
||||
test('.teardown() is bound', t => {
|
||||
const teardownCallback = sinon.spy();
|
||||
return ava(a => {
|
||||
const {teardown} = a;
|
||||
teardown(teardownCallback);
|
||||
a.pass();
|
||||
}).run().then(result => {
|
||||
t.true(result.passed);
|
||||
t.ok(teardownCallback.calledOnce);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue