0.3.0 • Published 8 months ago

jest-teardown v0.3.0

Weekly downloads
-
License
MIT
Repository
-
Last release
8 months ago

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 as afterEach
  • When called in a beforeAll, it'll run as afterAll
  • 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 and beforeAll 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:
    describe('my tests', () => {
      const userId = useTestUser();
    });
    There are creative ways to work around this, but none of these are particularly straightforward.

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

  1. 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.
  2. 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!
0.3.0

8 months ago

0.2.5

12 months ago

0.2.4

12 months ago

0.2.3

12 months ago

0.2.2

12 months ago

0.2.1

12 months ago

0.2.0

12 months ago