Skip to content

Commit 75cbc3b

Browse files
ulkennovemberborn
andauthoredApr 26, 2020
Add t.teardown()
Co-Authored-By: Mark Wubben <mark@novemberborn.net>
1 parent 993bb90 commit 75cbc3b

File tree

8 files changed

+218
-4
lines changed

8 files changed

+218
-4
lines changed
 

‎docs/01-writing-tests.md

+2
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ AVA lets you register hooks that are run before and after your tests. This allow
167167

168168
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.
169169

170+
*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.
171+
170172
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.
171173

172174
`.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:

‎docs/02-execution-context.md

+12-4
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,6 @@ Contains shared state from hooks.
2626

2727
Whether a test has passed. This value is only accurate in the `test.afterEach()` and `test.afterEach.always()` hooks.
2828

29-
## `t.plan(count)`
30-
31-
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).
32-
3329
## `t.end()`
3430

3531
End the test. Only works with `test.cb()`.
@@ -38,6 +34,18 @@ End the test. Only works with `test.cb()`.
3834

3935
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.
4036

37+
## `t.plan(count)`
38+
39+
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).
40+
41+
## `t.teardown(fn)`
42+
43+
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.
44+
45+
You cannot perform assertions using the `t` object or register additional functions from inside `fn`.
46+
47+
You cannot use `t.teardown()` in hooks either.
48+
4149
## `t.timeout(ms)`
4250

4351
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.

‎docs/recipes/test-setup.md

+2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ test('second scenario', t => {
6464
});
6565
```
6666

67+
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).
68+
6769
## A practical example
6870

6971
```js

‎index.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ export interface ExecutionContext<Context = unknown> extends Assertions {
313313

314314
log: LogFn;
315315
plan: PlanFn;
316+
teardown: TeardownFn;
316317
timeout: TimeoutFn;
317318
try: TryFn<Context>;
318319
}
@@ -344,6 +345,11 @@ export interface TimeoutFn {
344345
(ms: number): void;
345346
}
346347

348+
export interface TeardownFn {
349+
/** Declare a function to be run after the test has ended. */
350+
(fn: () => void): void;
351+
}
352+
347353
export interface TryFn<Context = unknown> {
348354
/**
349355
* Attempt to run some assertions. The result must be explicitly committed or discarded or else

‎lib/runner.js

+1
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ class Runner extends Emittery {
283283
metadata: task.metadata,
284284
powerAssert: this.powerAssert,
285285
title: `${task.title}${titleSuffix || ''}`,
286+
isHook: true,
286287
testPassed
287288
}));
288289
const outcome = await this.runMultiple(hooks, this.serial);

‎lib/test.js

+35
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ class ExecutionContext extends assert.Assertions {
6868
test.timeout(ms);
6969
};
7070

71+
this.teardown = callback => {
72+
test.addTeardown(callback);
73+
};
74+
7175
this.try = async (...attemptArgs) => {
7276
const {args, buildTitle, implementations, receivedImplementationArray} = parseTestArgs(attemptArgs);
7377

@@ -193,12 +197,14 @@ class Test {
193197
this.experiments = options.experiments || {};
194198
this.failWithoutAssertions = options.failWithoutAssertions;
195199
this.fn = options.fn;
200+
this.isHook = options.isHook === true;
196201
this.metadata = options.metadata;
197202
this.powerAssert = options.powerAssert;
198203
this.title = options.title;
199204
this.testPassed = options.testPassed;
200205
this.registerUniqueTitle = options.registerUniqueTitle;
201206
this.logs = [];
207+
this.teardowns = [];
202208

203209
const {snapshotBelongsTo = this.title, nextSnapshotIndex = 0} = options;
204210
this.snapshotBelongsTo = snapshotBelongsTo;
@@ -457,6 +463,34 @@ class Test {
457463
this.timeoutTimer = null;
458464
}
459465

466+
addTeardown(callback) {
467+
if (this.isHook) {
468+
this.saveFirstError(new Error('`t.teardown()` is not allowed in hooks'));
469+
return;
470+
}
471+
472+
if (this.finishing) {
473+
this.saveFirstError(new Error('`t.teardown()` cannot be used during teardown'));
474+
return;
475+
}
476+
477+
if (typeof callback !== 'function') {
478+
throw new TypeError('Expected a function');
479+
}
480+
481+
this.teardowns.push(callback);
482+
}
483+
484+
async runTeardowns() {
485+
for (const teardown of this.teardowns) {
486+
try {
487+
await teardown(); // eslint-disable-line no-await-in-loop
488+
} catch (error) {
489+
this.saveFirstError(error);
490+
}
491+
}
492+
}
493+
460494
verifyPlan() {
461495
if (!this.assertError && this.planCount !== null && this.planCount !== this.assertCount) {
462496
this.saveFirstError(new assert.AssertionError({
@@ -670,6 +704,7 @@ class Test {
670704
this.clearTimeout();
671705
this.verifyPlan();
672706
this.verifyAssertions();
707+
await this.runTeardowns();
673708

674709
this.duration = nowAndTimers.now() - this.startedAt;
675710

‎test-tap/hooks.js

+20
Original file line numberDiff line numberDiff line change
@@ -546,3 +546,23 @@ test('shared context of any type', t => {
546546
});
547547
});
548548
});
549+
550+
test('teardowns cannot be used in hooks', async t => {
551+
let hookFailure = null;
552+
await promiseEnd(new Runner(), runner => {
553+
runner.on('stateChange', evt => {
554+
if (evt.type === 'hook-failed') {
555+
hookFailure = evt;
556+
}
557+
});
558+
559+
runner.chain.beforeEach(a => {
560+
a.teardown(() => {});
561+
});
562+
563+
runner.chain('test', a => a.pass());
564+
});
565+
566+
t.ok(hookFailure);
567+
t.match(hookFailure.err.message, /not allowed in hooks/);
568+
});

‎test-tap/test.js

+140
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ require('../lib/worker/options').set({});
55
const path = require('path');
66
const React = require('react');
77
const {test} = require('tap');
8+
const sinon = require('sinon');
89
const delay = require('delay');
910
const snapshotManager = require('../lib/snapshot-manager');
1011
const Test = require('../lib/test');
@@ -746,6 +747,133 @@ test('timeout is refreshed on assert', t => {
746747
});
747748
});
748749

750+
test('teardown passing test', t => {
751+
const teardown = sinon.spy();
752+
return ava(a => {
753+
a.teardown(teardown);
754+
a.pass();
755+
}).run().then(result => {
756+
t.is(result.passed, true);
757+
t.ok(teardown.calledOnce);
758+
});
759+
});
760+
761+
test('teardown failing test', t => {
762+
const teardown = sinon.spy();
763+
return ava(a => {
764+
a.teardown(teardown);
765+
a.fail();
766+
}).run().then(result => {
767+
t.is(result.passed, false);
768+
t.ok(teardown.calledOnce);
769+
});
770+
});
771+
772+
test('teardown awaits promise', t => {
773+
let tornDown = false;
774+
const teardownPromise = delay(200).then(() => {
775+
tornDown = true;
776+
});
777+
return ava(a => {
778+
a.teardown(() => teardownPromise);
779+
a.pass();
780+
}).run().then(result => {
781+
t.is(result.passed, true);
782+
t.ok(tornDown);
783+
});
784+
});
785+
786+
test('teardowns run sequentially in order', t => {
787+
const teardownA = sinon.stub().resolves(delay(200));
788+
let resolveB;
789+
const teardownB = sinon.stub().returns(new Promise(resolve => {
790+
resolveB = resolve;
791+
}));
792+
return ava(a => {
793+
a.teardown(() => teardownA().then(resolveB));
794+
a.teardown(teardownB);
795+
a.pass();
796+
}).run().then(result => {
797+
t.is(result.passed, true);
798+
t.ok(teardownA.calledBefore(teardownB));
799+
});
800+
});
801+
802+
test('teardown with cb', t => {
803+
const teardown = sinon.spy();
804+
return ava.cb(a => {
805+
a.teardown(teardown);
806+
setTimeout(() => {
807+
a.pass();
808+
a.end();
809+
});
810+
}).run().then(result => {
811+
t.is(result.passed, true);
812+
t.ok(teardown.calledOnce);
813+
});
814+
});
815+
816+
test('teardown without function callback fails', t => {
817+
return ava(a => {
818+
return a.throwsAsync(async () => {
819+
a.teardown(false);
820+
}, {message: 'Expected a function'});
821+
}).run().then(result => {
822+
t.is(result.passed, true);
823+
});
824+
});
825+
826+
test('teardown errors fail the test', t => {
827+
const teardown = sinon.stub().throws('TeardownError');
828+
return ava(a => {
829+
a.teardown(teardown);
830+
a.pass();
831+
}).run().then(result => {
832+
t.is(result.passed, false);
833+
t.is(result.error.name, 'TeardownError');
834+
t.ok(teardown.calledOnce);
835+
});
836+
});
837+
838+
test('teardown errors are hidden behind assertion errors', t => {
839+
const teardown = sinon.stub().throws('TeardownError');
840+
return ava(a => {
841+
a.teardown(teardown);
842+
a.fail();
843+
}).run().then(result => {
844+
t.is(result.passed, false);
845+
t.is(result.error.name, 'AssertionError');
846+
t.ok(teardown.calledOnce);
847+
});
848+
});
849+
850+
test('teardowns errors do not stop next teardown from running', t => {
851+
const teardownA = sinon.stub().throws('TeardownError');
852+
const teardownB = sinon.spy();
853+
return ava(a => {
854+
a.teardown(teardownA);
855+
a.teardown(teardownB);
856+
a.pass();
857+
}).run().then(result => {
858+
t.is(result.passed, false);
859+
t.is(result.error.name, 'TeardownError');
860+
t.ok(teardownA.calledOnce);
861+
t.ok(teardownB.calledOnce);
862+
t.ok(teardownA.calledBefore(teardownB));
863+
});
864+
});
865+
866+
test('teardowns cannot be registered by teardowns', async t => {
867+
const result = await ava(a => {
868+
a.teardown(() => {
869+
a.teardown(() => {});
870+
});
871+
a.pass();
872+
}).run();
873+
t.is(result.passed, false);
874+
t.match(result.error.message, /cannot be used during teardown/);
875+
});
876+
749877
test('.log() is bound', t => {
750878
return ava(a => {
751879
const {log} = a;
@@ -796,3 +924,15 @@ test('.end() is bound', t => {
796924
t.true(result.passed);
797925
});
798926
});
927+
928+
test('.teardown() is bound', t => {
929+
const teardownCallback = sinon.spy();
930+
return ava(a => {
931+
const {teardown} = a;
932+
teardown(teardownCallback);
933+
a.pass();
934+
}).run().then(result => {
935+
t.true(result.passed);
936+
t.ok(teardownCallback.calledOnce);
937+
});
938+
});

0 commit comments

Comments
 (0)
Please sign in to comment.