DEV Community

Cover image for Easily create mock data for unit tests ๐Ÿงช
Thomas King
Thomas King

Posted on

Easily create mock data for unit tests ๐Ÿงช

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 design pattern, which will allow easily created instances that can still be overridden with custom data.

Note: I will be using Jest as testing framework throughout the examples.

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

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

export interface Person {
  id: string;
  firstName: string;
  lastName: string;
  pets: Pet[];
}
Enter fullscreen mode Exit fullscreen mode

Creating mock data for these interfaces could look like this:

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);
});
Enter fullscreen mode Exit fullscreen mode

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

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.

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  • 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:

export class PetBuilder extends Builder<Pet> {
  setDefaults(): Pet {
    return { id: '1', name: 'Bella' };
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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

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);
});
Enter fullscreen mode Exit fullscreen mode

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:

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);
  });
});

Enter fullscreen mode Exit fullscreen mode

Note: 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.

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()],
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (6)

Collapse
 
dikamilo profile image
dikamilo

Looks clean. I will probably extend this to allow set multiple parameters at once, without unnecessary chain of with especially when you have bigger objects and need to set multiple object parameters:

withMultiple(params: Partial<T>): Builder<T> {
    this.intermediate = {...this.intermediate, ...params}
    return this;
}
Enter fullscreen mode Exit fullscreen mode
new PersonBuilder().with('firstName', 'test')
new PersonBuilder().withMultiple({'firstName': 'name', 'lastName': 'surname'})
Enter fullscreen mode Exit fullscreen mode
Collapse
 
kinginit profile image
Thomas King

Lovely addition, thanks!!

Always use whatever works best for your use case. Another possibility is to add custom methods to the concrete builders. E.g., when the first name and last name are often set together on a Person, then you could implement a withName method in the PersonBuilder.

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

  withName(firstName: string, lastName: string): PersonBuilder{
    this.intermediate.firstName = firstName;
    this.intermediate.lastName = lastName;
    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: Do not forget to mark the intermediate property as protected inside the Builder class when doing this.

Using the withName inside your unit tests:

const person = builder.withName('Marty', 'Daniels').build();
Enter fullscreen mode Exit fullscreen mode

Thanks for your input, much appreciated!

Collapse
 
stealthmusic profile image
Jan Wedel • Edited

I usually forget the power of typescript. We are using the pattern in both backend and frontend.

Usually we combine it with Object mother to allow a different set of defaults (aMinimalPerson(), aConpletePerson()).

Also, it gets more complicated when having nested objects where each level has its own builder but you want to create a complete defaults object from the base builder but also want to modify a single value in the lead object.

We create something like

PersonBuilder.aFullPerson().modifyAddress().withZipCode(12345).done().build().

Here, modifyAddress return a builder filled with all the data from the main builder. Done() return back the parent builder.

This is quite complicated to implement in Java, I would really like to see if there is an elegant solution for this in Typescript.

Collapse
 
kinginit profile image
Thomas King

I think it will become quite complex when you start mixing child/parent builders inside your chaining.
As suggested in my comment to @dikamilo, you could add custom methods to the PersonBuilder that provide several defaults (besides the actual setDefaults implementation). An example:

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

  personNamedJohn(): PersonBuilder {
    this.intermediate.firstName = 'John';
    return this;
  }

  petlessPerson(): PersonBuilder { 
    this.intermediate.pets = [];
    return this;
  } 
}
Enter fullscreen mode Exit fullscreen mode

You can then create a person with your custom method and if you want to change the pets property, just initialize one with the PetBuilder and use the with.

const pets = [new PetBuilder().with('name', 'Pet of John').build()];
const john = builder.personNamedJohn().with('pets', pets).build();
Enter fullscreen mode Exit fullscreen mode

I think this is more "KISS" and will keep your code more readable as well.

Feel free to provide feedback!

Collapse
 
stealthmusic profile image
Jan Wedel • Edited

As I mentioned before, we are heavily using builders for our test data for years and it is awesome.

Unfortunately, in reality data object are not that simple (as a person). We have heavily nested data structures, e.g.:

{
   "a": {
      "b": {
         "c": {
            "id": 5
         }
      }
   }
}
Enter fullscreen mode Exit fullscreen mode

And both a, b and c represent own domain types with it's own builders and each object has lots of properties in reality.

We often ended up in a scenario that looks like this:

const a = ABuilder.anA().withB(
   BBuilder.aB().withC(
      CBuilder.aC().withId(5).build()
   ).build()
).build();
Enter fullscreen mode Exit fullscreen mode

Actually, ABuilder.anA().build()would already create all children as defaults, using builders as you did in your example. However, we need to change only the id of C in our test case. In reality, those objects are way more complex in our case and we end up having a lot of code in our test cases. To improve readability, we moved that code into helper functions.

So we had builders to not repeat our selves and we had object mother function to create a valid object but in the end, we always had to customise code in every test case with seemingly unnecessary code.

That why we introduced that parent/child relation in the abstract builder. It's not pretty BUT we only had to do it once. Now we can write very elegant code that requires only the very necessary bits.

const a = ABuilder.anA()
   .modifyB()
      .modifyC()
         .withId(5)
         .done()
      .done()
   .build();
Enter fullscreen mode Exit fullscreen mode

We are very happy with this but you might not need it for rather simple objects as in your example.

Collapse
 
ricardogesteves profile image
Ricardo Esteves

Nice and clean! Thanks for sharing it. ๐Ÿ‘Œ