Using test hooks for shared fixtures in Jest

I’ve been working on a project that needed some integration tests. Before any tests in a suite run, I wanted to open a connection to the database and do some other setup. At the end of the suite, I wanted to close the connection and tear down cleanly.

Jest has beforeAll and afterAll hooks that run before and after all tests in a test suite, which is basically what I wanted. But I don’t want to copy and paste the same code into every test file. I could of course just export some shared functions, but anytime someone adds a new test they have to remember to implement beforeAll and afterAll correctly. That seems annoying: I really wanted a “mixin” that I could easily bring in that would automatically apply the hooks to the test suite.

I stumbled across this post from Kristian Dupont that held the solution I wanted! Thanks Kristian!

The idea copies the pattern of React hooks. You can define a custom useTestFixture function that sets up the beforeAll and afterAll callbacks and returns a test fixture.

Since the fixture is actually instantiated in the beforeAll hook, you wind up having to return a function that fetches the fixture. This is a little clunky but I think the tradeoff is worth it.

I have my hook return an instance of a custom class that has useful helper methods for more easily writing the tests.

Here’s a representative sample:

test-fixture.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

export function useTestFixture() {
let fixture: TestFixture | undefined;

beforeAll(async () => {
// in beforeAll, do your initialization code, for example opening db connections,
// starting servers, etc.
const db = await mongoClient.connect(); // or whatever

// instantiate a fixture class to hold the references to test services
fixture = new TestFixture(db);
});

afterAll(async () => {
// Safe access (?.) is needed because the beforeAll hook might have thrown before
// instantiating the fixture
await fixture?.close();
});

// the hook returns a function that tests can use to get access to the fixture
// we can't just return the fixture because it would be undefined until after
// the beforeAll hook runs
return () => fixture!;
}

export class TestFixture {
constructor(public readonly db: Db){}

public async close() {
await this.db.close();
}

// Add useful helper methods here

async insertUser(user: User) {
await this.db.collection('users').insertOne(user);
}
}

My real-life beforeAll hook is a bit more full-featured. It includes customizations to:

  • Start up the mongo database client
  • Start up the express server
  • Start up the socket.io server
  • Set up mocks for AWS SDK calls
  • Make sure the test run gets a new virtual user, and that the user document exists in the database

My actual TestFixture class contains a bunch of useful helpers, such as

  • inserting and fetching users
  • making requests using supertest
  • A promisified wrapper around socket.io-client’s emit operation
  • shutting everything down cleanly
  • Nice methods to set up common mocks, such as signInAsAdmin

To use it, just import the hook and call it in your test file, inside the top-level describe block:

user-controller.spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
describe('UserController', () => {
// Set up the fixture, notice I don't have to implement any
// beforeAll or afterAll hooks here, it's done for me by the hook!
const getFixture = useTestFixture();

it('does something', async () => {
// you have to call this in each `it` block to get the fixture
const fixture = getFixture();

// use the fixture to get common state or mocks for your test subjects
const controller = new UserController(fixture.db)

const response = await controller.register({ ... });

// use the fixture's helper methods to help verify the results
const saved = await fixture.getUser(response.id);
expect(saved).toEqual({ ... });
})
})

When tests are easy to write, engineers are more likely to write them. If they have to write a lot of fiddly setup code in each test, they just throw up their hands and poke at it in the browser. To encourage writing tests, it’s important to provide frameworks that make it trivial to implement high-value test suites.

This trick is a great way to do that. It papers over the fiddly setup bits and provides testing utilities that reduce how much engineers have to write.