Michal Zalecki
Michal Zalecki
software development, testing, JavaScript,
TypeScript, Node.js, React, and other stuff

Fixtures, the way to manage sample and test data

Fixtures are a thin abstraction layer over sample data in your application which allows for better organizing, often complex, data structures representing different entities. If this sounds like a vague description or does not ring the bell maybe an example will speak to you better.

Mocking

The first scenario considers mocking. You might happen to hear that mocks are a bad idea and you would hear right! You definitely do not want to use too many mocks in your tests as you end up with a test suit barely testing anything, or more accurately, testing mocks. Mocking that I have in mind is mocking an external API endpoint during development. You might want to do that to limit access to a service which bills you for a throughput or because backend provided only documentation without yet working implementation.

Instead of using an actual endpoint you can come up with an alternative solution which temporarily provides you with a set of resources so you can continue with development.

fetch("http://example.com/clients")
  .then(response => response.json())
  .then(clients => {
    // do something with clients
  })

// TODO: Use an actuaal endpoint when API is ready
Promise.resolve([...Array(10)].map(() => clientFixture()))
  .then(clients => {
    // do somethign with clients
  })

You may consider using some abstraction which groups access to available endpoints under a unified interface. That would better separate concerns (fetching, errors handling, processing) and maybe change behavior based on production/development environment or a feature flag but this is beyond the scope of this article.

Testing on "real" data

Imagine that you have to filter clients which agreed for receiving email notifications and return the list of their emails. If you start writing your tests already assuming that function is interested only in two fields: email and emailNotifications you may end up with the following test.

describe("Clients mappers", () => {
    describe("getEmailsToNotify", () => {
        it("returns emails of clients who agrred to receive notifications", () => {
            const clients = [
                { email: "[email protected]", emailNotifications: true },
                { email: "[email protected]", emailNotifications: false },
                { email: "[email protected]", emailNotifications: true },
            ];

            expect(getEmailsToNotify(clients)).toEqual([
                "[email protected]",
                "[email protected]",
            ])
        })
    })
})

The test is simple, easy to read, quick to execute, does not cause any side effects and does not rely on network resources. At first sight, it looks ok. The problem with this test is that it is not very concrete. This is not a data structure we can use in an actual application.

Now we want to change our implementation so we return email together with a name which can be displayed by an email client instead of a raw address.

describe("Clients mappers", () => {
    describe("getEmailsToNotify", () => {
        it("returns names and emails of clients who agrred to receive notifications", () => {
            const clients = [
                { full_name: "John Doe", email: "[email protected]", emailNotifications: true },
                { full_name: "John Doe", email: "[email protected]", emailNotifications: false },
                { full_name: "John Doe", email: "[email protected]", emailNotifications: true },
            ];

            expect(getEmailsToNotify(clients)).toEqual([
                "\"John Doe\" <[email protected]>",
                "\"John Doe\" <[email protected]>",
            ])
        })
    })
})

We adjusted an expected result but after we have changed an implementation we also had to go back and change data we use for testing. This is less than ideal as it makes you switch back and forth. The other disadvantage is that implementation starts to drive data when it should be the opposite. You can easily refactor your tests using fixtures:

describe("Clients mappers", () => {
    describe("getEmailsToNotify", () => {
        it("returns emails of clients who agrred to receive notifications", () => {
            const clients = [
                clientFixture({ email: "[email protected]" }),
                clientFixture({ email: "[email protected]", emailNotifications: false }),
                clientFixture({ email: "[email protected]" }),
            ];

            expect(getEmailsToNotify(clients)).toEqual([
                "\"John Doe\" <[email protected]>",
                "\"John Doe\" <[email protected]>",
            ])
        })
    })
})

Implementing fixtures

Fixtures implementation is a piece of cake. The most difficult part is coming up with reasonable defaults. E.g. creating each new client with emailNotifications set to true.

import uuidv4 from "uuid/v4";

interface RawClient {
  id: string;
  full_name: string;
  email: string;
  email_notifications: boolean;
}

function clientFixture(props: Partial<RawClient> = {}): RawClient {
  const defaults: RawClient = {
    id: uuidv4(),
    full_name: "John Doe",
    email: "[email protected]",
    email_notifications: true,
  };

  return { ...defaults, ...props };
}

const client = clientFixture();

I am showing an implementation in TypeScript as it better represents resulting types. It is important to use not only compatible but the same type definition as used in the application code. Type is called RawClient not without a reason. I like to keep fixtures so they relate to what I fetch from the server. The format received from the server might not be the same you decided to use internally. I am using mappers to create an additional layer which lets me have more control over the shape of data I send or fetch. You can read more about mappers.

DRY

Not repeating yourself is more an outcome than a reason of using fixtures in the first place. Eliminating repetition is the result of having fixtures functions which are simply factories for a given type. You can easily compose different fixtures to represent associations on nested data.

const mailingList = mailingListFixture({
    recurring: true,
    clients: [clientFixture(), clientFixture()],
})

Wrap up

I come across fixtures for the first time while testing rails applications. In general, I recommend skimming through Rails conferences' agenda as talks are often full of interesting software engineering solution. I cannot say that fixtures are vastly popular in JavaScript land but if simple functions will not meet your expectations you can still find something on npm for yourself. Recently, React specific react-cosmos is getting an attention.

Photo by Paul Felberbauer on Unsplash.