jest-teardown
v0.3.0
Published
Unified teardown hook for Jest that enables bundling of setup & teardown work into reusable functions.
Downloads
317
Readme
jest-teardown
Cleanup from anywhere.
jest-teardown
is a unified teardown hook for Jest that enables bundling of setup & teardown work into reusable functions.
teardown
hooks are context-aware and trigger their cleanup at the end of the current scope:
- When called in a
beforeEach
, it'll run asafterEach
- When called in a
beforeAll
, it'll run asafterAll
- When called in a test, it'll run at the end of the test
This allows putting setup & teardown together in reusable utility functions, which can then be re-used wherever needed.
Usage
Using the example from the Jest documentation: we have an initializeCityDatabase
setup method and a clearCityDatabase
teardown method:
Idiomatic usage
// test-utils/city-database.js
import { teardown } from 'jest-teardown';
export function useCityDatabase() {
initializeCityDatabase();
teardown(() => clearCityDatabase());
}
// my-test.spec.js
import { useCityDatabase } from './test-utils/city-database';
// setup runs in `beforeEach`, teardown in `afterEach`
beforeEach(() => useCityDatabase());
// setup runs in `beforeAll`, teardown in `afterAll`
beforeAll(() => useCityDatabase());
// setup runs at start of test, teardown at the end of the test
test('my test', () => {
useCityDatabase();
/* rest of the test */
});
One-off usage
For the cases where the setup & teardown are specific to a single test or file, and you don't want to extract it to a utility.
import { teardown } from 'jest-teardown';
beforeEach(() => {
initializeCityDatabase();
teardown(() => clearCityDatabase()); // will run in `afterEach`
});
beforeAll(() => {
initializeCityDatabase();
teardown(() => clearCityDatabase()); // will run in `afterAll`
});
test('my test', () => {
initializeCityDatabase();
teardown(() => clearCityDatabase()); // will run after 'my test' completes
// the rest of the test
});
Motivation
Out-of-the-box, jest
provides us with some common setup and teardown hooks. While the setup hooks are great, the teardown hooks are ... less so.
In a typical case, a teardown hook cleans up something that's been created in their matching setup hook.
E.g. afterEach
cleans up beforeEach
, and afterAll
cleans up beforeAll
.
This creates an implicit coupling between the hooks, which causes unnecessary complexity and boilerplate in tests.
To illustrate, let's take the example from Jest's documentation:
beforeEach(() => {
initializeCityDatabase();
});
afterEach(() => {
clearCityDatabase();
});
The first issue is that it's easy to forget adding the teardown hook. And when we forget, this can cause failures in completely unrelated tests that follow later.
The second issue is that we'll end up duplicating snippets when multiple tests have similar setup needs, without a good way of abstracting this.
The third issue is that sharing state between the setup to the teardown is rather convoluted, as it needs to be passed through exposed variables in a higher (unrelated) scope.
// Server isn't accessed by the tests, but we're still forced to keep track of it for the `afterEach` hook.
let server;
beforeEach(() => {
server = initializeTestServer();
});
afterEach(() => {
server?.shutdown();
});
The fourth issue is that while we have teardown hooks for "all" and "each" tests, we don't have teardown hooks for individual test. Instead, we'll manually need to teardown using try-finally
constructs.
// If we only need our teardown for some isolated test(s),
// then we'll need to write something boilerplate-heavy like this:
it('does something', () => {
let server;
try {
server = initializeTestServer();
/* The actual test */
} finally {
server?.shutdown();
}
});
Normally when we're dealing with repetitive code or shared state, we would encapsulate this. We could try this with hooks, but we'll soon find out that this doesn't work very well:
function useCityDatabase() {
beforeEach(() => {
initializeCityDatabase();
});
afterEach(() => {
clearCityDatabase();
});
}
describe('my test suite', () {
useCityDatabase();
});
We're very quickly running into problems here:
- If we want to support both
beforeEach
andbeforeAll
then we'll need to write multiple flavors of the same function. E.g.useCityDatabaseEach
/useCityDatabaseAll
/useCityDatabase({ scope: 'each'|'all' })
. - We still can't use this for single tests. We could create yet another variant like
withCityDatabase(() => { /* the test */ })
, but this doesn't stack very well. Imagine needing a few of these for a single test, and you'll see the problem. - It complicates passing variables from hooks to tests. Let's say that our
beforeEach
creates a test user and we need the users id in our tests. This won't work:
There are creative ways to work around this, but none of these are particularly straightforward.describe('my tests', () => { const userId = useTestUser(); });
The solution
What we really need is a way to attach a teardown hook to some setup, which then automatically runs at the right time. This is what jest-teardown
does:
import { teardown } from 'jest-teardown';
beforeEach(() => {
initializeCityDatabase();
teardown(() => clearCityDatabase()); // will run in `afterEach`
});
beforeAll(() => {
initializeCityDatabase();
teardown(() => clearCityDatabase()); // will run in `afterAll`
});
test('my test', () => {
initializeCityDatabase();
teardown(() => clearCityDatabase()); // will run after 'my test' completes
// the rest of the test
});
The real benefit is that it now enables us to encapsulate this trivially!
import { teardown } from 'jest-teardown';
function useCityDatabase() {
initializeCityDatabase();
// We don't need to know if it's called in a beforeEach, beforeAll, or in a test. jest-teardown handles that for us.
teardown(() => clearCityDatabase());
}
beforeEach(() => useCityDatabase());
beforeAll(() => useCityDatabase());
test('my test', () => {
useCityDatabase();
/* test logic */
});
This even works with shared state between setup and teardown:
import { teardown } from 'jest-teardown';
function useTestServer() {
const server = initializeTestServer();
teardown(() => server.shutdown());
}
beforeEach(() => useTestServer());
beforeAll(() => useTestSErver());
test('my test', () => {
useTestServer();
// The rest of the test
});
And exposing variables to tests works too:
import { teardown } from 'jest-teardown';
function useTestUser() {
const user = initializeTestUser();
teardown(() => removeTestUser(user));
return user;
}
let user;
beforeEach(() => user = initializeTestUser());
beforeAll(() => user = initializeTestUser());
test('my test', () => {
const user = initializeTestUser();
});
The fine print
teardown
does not work in.concurrent
tests. We need to use some shared global state behind the scenes to make it work, which we cannot do in concurrent tests.jest-teardown
needs to monkey-patch some of the Jest methods to work. If you find that it breaks something, please file an issue here!