npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

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 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!