A simple, readable, meaningful test style with Jest

I usually write tests with a Given, When, Then structure to keep the test focused on documenting and confirming the important behaviour of whatever is being tested. Simple comments are good for this and provide an easy-to-follow visual structure:

test("some defined behaviour", () => {
    // Given the fandango is combobulated;

    // When we discombobulate the fandango;

    // Then the fandango should be discombobulated.

})

It’s important to keep tests clear and readable. Reducing the number of lines under each step and making each line as meaningful as possible are the main ways to do that. You can use test factories and other extractions to reduce line-count and make test code more meaningful.

Another approach I’ve been using in tests is to extract each step as a suitably named step function, so that the test ends up looking like this:

test("some defined behaviour", () => {
    // Given there is a combobulated fandango;
    const fandango = await thereIsACombobulatedFandango()

    // When we discombobulate the fandango;
    await fandangoDiscombobulator.discombobulateFandango(fandango.id)

    // Then the fandango should be discombobulated.
    await theFandangoHasBeenDiscombobulated(fandango.id)
})

async function thereIsACombobulatedFandango(): Promise<Fandango> {
    return makeFandango({ combobulated: true })
}

async function theFandangoHasBeenDiscombobulated(
    fandangoId: string
): Promise<void> {
    // ...some async work...
    expect(fandango.combobulated).toBe(false)
}

The extracted steps are trivial, but when there is more realistic complex work happening in each step, this improves the readability of the test. Each step is now meaningful and relevant to confirming the behaviour we’re interested in here.

A lot of testing frameworks, especially BDD ones, have this kind of reusable test step. However, the potential for re-use isn’t actually the main goal or benefit here. The main benefit of doing this is to make the test code more meaningful.

Notice that the step functions are not exported – we’re primarily interested in extracting semantic steps for this test alone. If we happen to be able to re-use the same steps elsewhere, that could be a nice side-benefit, but we’d need to be careful not to let them attract cruft as more and more logic gets added to the most popular test steps. Keeping straightforward test steps right next to the test they serve is usually more beneficial.


Tech mentioned