Building Mock Data Factories for Unit Testing in Typescript.

Building Mock Data Factories for Unit Testing in Typescript.

When I started using NestJS for Backend web development and coming from Ruby on Rails, I wanted an easy way, much like the Factorybot gem, to generate mock data to use with jest.

I did a quick scan for packages at that time and found none that provided a quick way for this easy task, so after missing RoR for a minute, I wrote a small factory that I wanted to share with you.

Design Process

Let's assume we have a model called Posts with those fields:

userId: ID of the author
status: PUBLISHED, DRAFT, REJECTED
featured: Boolean flag
title: Title of the Post
content: Some MD content

What I was aiming for while writing my specs is something like this:

const mockPost = MockPostFactory.getOne(["published"], {
  userId: "MOCK_USER_ID_1",
});
jest.spyOn(postsService, 'findOne').mockResolvedValue(mockPost);

And if I'd like to get a published and featured post, for example, I can do so with:

const mockPost = MockPostFactory.getOne(["published", "featured"], {
  userId: "MOCK_USER_ID_1",
});
jest.spyOn(postsService, 'findOne').mockResolvedValue(mockPost);

It's safe to assume, as fields increase, there will be a lot of possible combinations of "traits" for our post object, but the "fields" themselves are a few.

So. Decorators.

Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.

Checkout this article for more explanation and examples.

If post is an object with the bare-metal default values, published is a function that takes a post and changes the status field to PUBLISHED and returns a new post, and lastly, featured takes a post changes the featured flag and returns the new post.

Then in our case, we can describe a Published Post as published(post), Featured Post as featured(post), and Published Featured Post as published(featured(post)).

This way, we don't need to handle all combinations but define how each trait (decorator) changes the default object.

Code

All right enough talking, let's write some code.

As mentioned above, we'll define our objects as a series of nested decorators, and at the end, there will always be a default object (our base case).

So first, let's define our default object. And I'm legally obliged by the laws of Tutorials to have everything in one big file, sooo:

enum PostStatus {
  PUBLISHED = 'PUBLISHED',
  DRAFT = 'DRAFT',
  REJECTED = 'REJECTED',
}

class MockPostFactory {
  private static default(overridedFields?: Record<string, unknown>): Record<string, unknown> {
    return {
      userId: 'MOCK_USER_ID',
      status: PostStatus.DRAFT,
      featured: false,
      title: 'Mock title',
      content: '#Hello World',
      ...overridedFields,
    };
  }
}

overridedFields fields here give the ability to override some fields explicitly by the user of our factory.

Note: If you are unfamiliar with this syntax check out Destructuring and other handy JS tricks here.

Alright, second, let's write our decorators, here it'll be just a function that takes an object and returns a new one, nothing fancy.


class MockPostFactory {
  .....

  private static published(
    base: Record<string, unknown>,
    overridedFields?: Record<string, unknown>,
  ): Record<string, unknown> {
    return {
      ...base,
      status: PostStatus.PUBLISHED,
      ...overridedFields,
    };
  }

  private static draft(
    base: Record<string, unknown>,
    overridedFields?: Record<string, unknown>,
  ): Record<string, unknown> {
    return {
      ...base,
      status: PostStatus.DRAFT,
      ...overridedFields,
    };
  }

  private static rejected(
    base: Record<string, unknown>,
    overridedFields?: Record<string, unknown>,
  ): Record<string, unknown> {
    return {
      ...base,
      status: PostStatus.REJECTED,
      ...overridedFields,
    };
  }

  private static featured(
    base: Record<string, unknown>,
    overridedFields?: Record<string, unknown>,
  ): Record<string, unknown> {
    return {
      ...base,
      featured: true,
      ...overridedFields,
    };
  }
}

Same thing with destructuring and overriding value.

Now putting everything together with the help of reduce:


enum MockPostTraits {
  PUBLISHED = 'published',
  DRAFT = 'draft',
  REJECTED = 'rejected',
  FEATURED = 'featured',
}

class MockPostFactory {
  static getOne(
    traits: MockPostTrait[] = [],
    overridedFields?: Record<string, unknown>,
  ) {
    const mock = traits.reduce(
      (acc: Record<string, unknown>, trait: MockPostTrait) => {
        return MockPostFactory[trait](acc);
      },
      MockPostFactory.default(),
    );

    return {
      ...mock,
      ...overridedFields,
    }
  }
 ....
}

in getOne and with reduce we initialize acc with the default base object we have then we iterate over each trait.

For each trait, we get the function (which is an attribute of the class MockPostFactory) and call it with the acc which will be the return of the previous decorator.

For example if we trace our code when we call getOne([MockPostTraits.PUBLISHED, MockPostTraits.FEATURED])

It'll be something like this:

- acc = default()

- Reduce First Iteration
trait = MockPostTraits.PUBLISHED
MockPostFactory[trait] = published function
new acc = published(acc) // where acc is default

- Reduce Second Iteration
trait = MockPostTraits.FEATURED
MockPostFactory[MockPostTraits.FEATURED] = featured function
new acc = featured(acc) // where acc is published(default)

- Return featured(published(default));

And the output would be:

{
  userId: 'MOCK_USER_ID',
  status: 'PUBLISHED',
  featured: true,
  title: 'Mock title',
  content: '#Hello World'
}

And one last thing, let's make getList and for this, we will use lodash times function (because I like lodash) and destructuring again but this time to make a shallow copy. You should consider another way to clone if the mock data are nested.

class MockPostFactory {
  .....

  static getList(
    count: number,
    traits: MockPostTraits[] = [],
    overridedFields?: Record<string, unknown>,
  ) {
    const oneMock = MockPostFactory.getOne(traits, overridedFields);
    return _.times(count, () => ({ ...oneMock }))
  }

And finally here's our complete code so far:

import * as _ from 'lodash';

enum PostStatus {
  PUBLISHED = 'PUBLISHED',
  DRAFT = 'DRAFT',
  REJECTED = 'REJECTED',
}

enum MockPostTrait {
  PUBLISHED = 'published',
  DRAFT = 'draft',
  REJECTED = 'rejected',
  FEATURED = 'featured',
}

class MockPostFactory {
  static getOne(
    traits: MockPostTrait[] = [],
    overridedFields?: Record<string, unknown>,
  ) {
    const mock = traits.reduce(
      (acc: Record<string, unknown>, trait: MockPostTrait) => {
        return MockPostFactory[trait](acc);
      },
      MockPostFactory.default(),
    );

    return {
      ...mock,
      ...overridedFields,
    }
  }

  static getList(
    count: number,
    traits: MockPostTrait[] = [],
    overridedFields?: Record<string, unknown>,
  ) {
    const oneMock = MockPostFactory.getOne(traits, overridedFields);
    return _.times(count, () => ({ ...oneMock }))
  }

  private static default(
    overridedFields?: Record<string, unknown>,
  ): Record<string, unknown> {
    return {
      userId: 'MOCK_USER_ID',
      status: PostStatus.DRAFT,
      featured: false,
      title: 'Mock title',
      content: '#Hello World',
      ...overridedFields,
    };
  }

  private static published(
    base: Record<string, unknown>,
    overridedFields?: Record<string, unknown>,
  ): Record<string, unknown> {
    return {
      ...base,
      status: PostStatus.PUBLISHED,
      ...overridedFields,
    };
  }

  private static draft(
    base: Record<string, unknown>,
    overridedFields?: Record<string, unknown>,
  ): Record<string, unknown> {
    return {
      ...base,
      status: PostStatus.DRAFT,
      ...overridedFields,
    };
  }

  private static rejected(
    base: Record<string, unknown>,
    overridedFields?: Record<string, unknown>,
  ): Record<string, unknown> {
    return {
      ...base,
      status: PostStatus.REJECTED,
      ...overridedFields,
    };
  }

  private static featured(
    base: Record<string, unknown>,
    overridedFields?: Record<string, unknown>,
  ): Record<string, unknown> {
    return {
      ...base,
      featured: true,
      ...overridedFields,
    };
  }
}

Thanks for Reading!

And If you have any feedback or would like to improve on this please let me know in the comments :)