Easily create mock data for unit tests 🧪
Tags: unittest, jest, typescript, testing
Published: 2024-02-15 12:37:36

Whenever you write unit tests, the time comes that you have to mock data. Mocking this data can become tedious in many ways. Either the data gets copied over and over again for each unit test, or there is a file (or multiple files) that will contain all the mock data. If that data's interface (or class) suddenly changes, you will have to update each occurrence. You can use the IDE for most heavy lifting if you're lucky. 

With this guide, I will show you a way to easily create mock data for each interface or class you have. It is based on the [builder](https://refactoring.guru/design-patterns/builder/typescript/example#lang-features) design pattern, which will allow easily created instances that can still be overridden with custom data. 

> <u>Note</u>: I will be using [Jest](https://jestjs.io/) as testing framework throughout the examples.

As an example, let's start with two interfaces, `Pet` and `Person`.

```TypeScript
export interface Pet {
  id: string;
  name: string;
}

export interface Person {
  id: string;
  firstName: string;
  lastName: string;
  pets: Pet[];
}
```

Creating mock data for these interfaces could look like this:

```TypeScript
it('Person should have pets', () => {
  const pets: Pet[] = [{
    id: '1',
    name: 'Bella'
  }];

  const person: Person = {
    id: '1',
    firstName: 'John',
    lastName: 'Doe',
    pets,
  }
  expect(person.pets).toHaveLength(1);
});
```

Seems ok and not too much code, it could even be optimized more e.g., by using a for-loop. But, this is only for 1 unit test. If the `Person` interface will be used in another unit test (in another file), that same code will have to be recreated. The downsides of manually creating this data:
1. It is time-consuming
2. Prone to human errors (e.g., copy-pasting and forgetting to change something)
3. A large volume of mock data might become unscalable and hard to manage
4. It might add a lot of bloat to your unit testing code making it less readable

![copy-paste](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ql2gqzsfl04ceadtnlm2.gif)

Time to build a `PersonBuilder` and `PetBuilder` that will do the heavy lifting for us by providing default mock data. Let's start by creating an abstract Builder class that other builders can extend. 

```TypeScript
export abstract class Builder<T> {
  private intermediate: Partial<T> = {};

  constructor() {
    this.reset();
  }

  private reset(): void {
    this.intermediate = { ...this.setDefaults() } ;
  }

  abstract setDefaults(): Partial<T>;

  with<K extends keyof T>(property: K, value: T[K]): Builder<T> {
    this.intermediate[property] = value;
    return this;
  }

  build(): T {
    let p: Partial<T> = {};
    for (let key in this.intermediate) {
      p[key] = this.intermediate[key];
    }
    this.reset();
    return p as T;
  }
}
```

The abstract `Builder` class contains four methods:
- **reset**: to reset the internal object inside the builder back to the defaults. This is marked as private but can be marked as public as well if you prefer to use it outside your builder.
- **setDefaults**: the abstract method that will be implemented in the child Builders and that will provide the defaults for the object.
- **with**: the method that can provide overrides for your object. Returning `this` will make these methods chainable. Intellisense will be provided thanks to the `K extends keyof T`.
![with intellisense](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u7183g4457axb04malbt.png)
- **build**: the method that will provide the final instance.

With this base `Builder` class, creating child Builder classes becomes easy. All you have to do is extend from `Builder` with the appropriate generic type and implement the abstract method `setDefaults`.

Creating a `PetBuilder` will look like this:

```TypeScript
export class PetBuilder extends Builder<Pet> {
  setDefaults(): Pet {
    return { id: '1', name: 'Bella' };
  }
}
```

The `Person` interface contains a reference to `Pet`, so we can use the newly created `PetBuilder` to create a default pet for us inside the `PersonBuilder`.

```TypeScript
export class PersonBuilder extends Builder<Person> {
  setDefaults(): Person {
    return {
      id: '1',
      firstName: 'John',
      lastName: 'Doe',
      pets: [new PetBuilder().build()],
    };
  }
}
```

Updating our previous unit test with these builders will look like this:

```TypeScript
it('Person should have pets', () => {
  const pets: Pet[] = [new PetBuilder().build()];
  const person: Person = new PersonBuilder().with('pets', pets).build();
  expect(person.pets).toHaveLength(1);
});
```

We could even make it shorter by just removing the pets variable since the `PersonBuilder` already provides us with a default `pets` array containing 1 pet. But since this unit test tests for the presence of pets, we should include it as well.

Using the builder pattern, making overrides becomes easy:

```TypeScript
describe('Given a person', () => {
  let builder: PersonBuilder;
  beforeEach(() => {
    builder = new PersonBuilder();
  });
  it('with the first name Tim should have that name', () => {
    const person: Person = builder.with('firstName', 'Tim').build();
    expect(person.firstName).toBe('Tim');
  });

  it('with the first name Thomas should have that name', () => {
    const person: Person = builder.with('firstName', 'Thomas').build();
    expect(person.firstName).toBe('Thomas');
  });

  it('with the last name Doe should have no pets', () => {
    const person: Person = builder.with('lastName', 'Doe').with('pets', []).build();
    expect(person.pets).toHaveLength(0);
  });
});

```

> <u>Note</u>: I know these unit tests make no sense from a testing perspective, but it is purely an example of the builders.

## Bonus: adding Faker

Instead of manually having to think of defaults for your interface properties, you could use [Faker](https://fakerjs.dev/).

```TypeScript
import { faker } from '@faker-js/faker';

export class PetBuilder extends Builder<Pet> {
  setDefaults(): Pet {
    return { 
      id: faker.string.uuid(), 
      name: faker.person.firstName() };
  }
}

export class PersonBuilder extends Builder<Person> {
  setDefaults(): Person {
    return {
      id: faker.string.uuid(),
      firstName: faker.person.firstName(),
      lastName: faker.person.lastName(),
      pets: [new PetBuilder().build()],
    };
  }
}
```

Using Faker in your unit tests will create randomly generated data, which is nice but during testing, you'll want reproducible results. Luckily Faker provides something precisely for that: `faker.seed(123);`. Using a seed will provide reproducible results when running your unit tests. 

You can add this seed in your global setup or use a `beforeAll`. 

I hope this guide will make your life of mocking data a bit easier. If you have any questions, feel free to reach out!