Flexible test model factories in TypeScript

Tests are improved by being more readable, and concise tests are more readable.

It’s good when a test has clear given, when, then steps, and the important details are plain to see in the test code.

This is often obstructed by large amounts of set-up and boilerplate code in the test that distracts from the important details.

Test model factories can reduce some of that boilerplate.

For example, if we have a Client model that contains quite a lot of detail, setting one up in a test will be verbose. We might only be interested in testing the effect of a single field in a single test, but setting up all the other required fields obscures that intention.

It’s clearer if we only specify the interesting fields for this test:

// Given we have a client over the age of 18;
const client = makeClient({ biographic: { dateOfBirth: "1990-01-01" } });

// ... now it's clear what is important for the rest of the test.

This can be a bit tricky to do with strong typing in TypeScript, as you want to enforce that the client is set up correctly, but also allow tweaking only the particular fields that are relevant to a given test.

The built-in Partial utility type gets you most of the way there. With some help from StackOverflow, we can get a RecursivePartial type.

The factory function signature looks like this:

function makeClient(properties?: RecursivePartial<Client>): Client

This lets you strongly type the Client structure, and also allow typed but individual overrides of particular parts. You can pass in just the properties you want to override, or none at all and use all of the defaults.

The implementation is like this:

const faker = require('faker/locale/en_GB')
import * as _ from 'lodash'

function makeClient(properties?: RecursivePartial<Client>): Client {
    const defaults: Client = {
        firstName: faker.name.firstName(),
        lastName: faker.name.lastName(),
        biographic: {
            dateOfBirth: moment.utc().subtract(Math.random() * 80 + 18, 'years')
        }
    }
    const finalProperties = _.merge(defaults, properties)
    const client: Client = new Client()
    for (const key of Object.keys(finalProperties)) {
        client[key] = finalProperties[key]
    }
    return client
}

Default valid client properties are set up using the faker library. Then lodash merge is used to replace any given oveßrrides from the passed in properties. The result is an overridable client factory that allows for concise tests and maintains type safety.


Tech mentioned