DEV Community

Cover image for Mock-Factory-Pattern in TypeScript
David Losert
David Losert

Posted on • Updated on

Mock-Factory-Pattern in TypeScript

If you are writing automated tests (and I sure hope you do), you most likely also create a lot of mock-data to feed into the functions under test. With this post I want to show you how to do this in a scalabe, reusable and focused way by using a mock-factory.

The initial simple way

When starting a new project or test-suite, creating mock-data usually starts out very simple by using object literals:

it('markTodoAsDone sets done to true.', () => {
  const unfinishedTodo: Todo = {
    title: 'Write an awesome post about Testdata-Patterns',
    done: false
  };

  const resultingTodo = markTodoAsDone(unfinishedTodo);

  assert.deepStrictEquals(resultingTodo.done, true);
})
Enter fullscreen mode Exit fullscreen mode

However, as your project grows, your data usually grows as well. New properties and behaviours are added, and you will quickly realize that the method above does not scale well. Everytime a new property is introduced, you have to revisit every single test and adjust the testdata-object. Depending on the size of your project, that might be a dozen or even hundreds of required adjustments because of a single change.

But the example above actually has another issue - can you spot it?

Maybe it becomes clearer once we actually add some new properties:

it('markTodoAsDone sets done to true.', () => {
  const unfinishedTodo: Todo = {
    title: 'Write an awesome post about Testdata-Patterns',
    done: false,
    dueDate: new Date('2021-07-04'),
    assignee: 'David Losert',
    project: 'Writing Blogposts',
    tags: ['writing', 'blogs']
  };

  const resultingTodo = markTodoAsDone(unfinishedTodo);

  assert.deepStrictEquals(resultingTodo.done, true);
Enter fullscreen mode Exit fullscreen mode

Right, with pure object literals you actually have to specifiy all the properties of the object all the time - even if only one of those is relevant for the unit under test. That is a lot of distraction. Tests should be precise, focused and only contain the data and properties currently required.

Alternative ways

So what is the alternative, you might ask? I have seen quite some, but they usually only address parts of the problem or introduce new ones:

  1. Putting test-objects into their own files This might remove the distraction - but it also removes the property the test is about. Also, it does not help you with the sclaing issue - on the contrary. Creating a file for every test or maybe different test scenarios, you now have to go into every file whenever something on the source type is changed. And it becomes quite a mess pretty fast.

  2. Casting with TypeScript's as

      const unfinishedTodo = {
        done: false
      } as Todo;
    

    While this most certainly works, it leaves a bit of a bad taste as it is actually cheating the type system, thus openning the door to nasty and hard to track bugs. If the function under test expects a Todo, we should provide a full-fledged object and not just a partial one. Maybe the unit under test relies on a ceratin property not being undefined even though it's not really relevant for the test (thinking of a 'stub'). Plus you have to type as Thing everywhere which again is quite a bit of a distraction in my eyes.

  3. Spreading on a default object

      // In a separe file or on top of your test file...
      const defaultTodo: Todo = {
        title: 'Default Todo Title',
        done: false,
        dueDate: new Date('2021-07-04'),
        assignee: 'David Losert',
        project: 'Writing Blogposts',
        tags: ['writing', 'blogs']
      }
    
      it('markTodoAsDone sets done to true.', () => {
        const unfinishedTodo: Todo = {
          ...defaultTodo,
          done: false
        };
        // ...
      }
    

    This heads into a better direction. The test remains focused on the relevant properties, yet our unit under test always gets a real Todo-object as opposed to when casting. It also scales somewhat: changes to the type only have to be applied to the defaultTodo-object. And we get another bonus: the defaultTodo serves as a great documentation on how a real Todo-object might look in production.

    There remains a tiny problem with this approach: Your tests are now tightly coupled to the defaultTodo-object, which might again introduce problems with - you might have guessed it - scaling. And I wouldn't write this if I did not know of a slightly better approach.

Enter the stage: TypeScripts Partial and a factory-function

So the way I usually go is a combination of TypeScript's Partial and a simple factory function. I'll show you the code before going into the details.

createMockTodo.ts:

const defaultTodo: Todo = {
  title: 'Default Todo Title',
  done: false,
  dueDate: new Date('2021-07-04'),
  assignee: 'David Losert',
  project: 'Writing Blogposts',
  tags: ['writing', 'blogs']
}

const createMockTodo = (overwrites: Partial<Todo> = {}) => ({
  ...defaultTodo,
  ...overwrites
});

export {
  createMockTodo
};
Enter fullscreen mode Exit fullscreen mode

The usage of might look like this:

todo.test.ts:

it('markTodoAsDone sets done to true.', () => {
  const unfinishedTodo: Todo = createMockTodo({ done: false });

  const resultingTodo = markTodoAsDone(unfinishedTodo);

  assert.deepStrictEqual(resultingTodo.done, true);
}
Enter fullscreen mode Exit fullscreen mode

So there is a few things happening here, let me walk you through the most important ones:

  • defaultTodo is basically the same as in the section before: A fully defined object of the type to make it easy to always get all (required) properties from a single place. Additional advantage: It serves as documentation about the type.
  • But rather than exporting this object directly, we use a factory function createMockTodo. This gives us way more control over how the actual mock-object is constructed. You will see how this helps us further down.
  • Partial<T> is a TypeScript utility type that accepts another Type or Interface as generic argument (if you don't know about generics, I recommend you read the official docs). It then creates a new type by copying all properties of the given generic input type but with all properties set to optional.

    This lets us pass in a object with only the properties of our choosing (as we did with done in the example) while at the same time keeping type-safety turned on. We can only pass things that actually exist in Todo.

  • We use an empty object as deafult argument to overwrites so that we can also call the factory-function without any parameters. This is especially useful if you just need some stubs in your test but don't care about the precise properties.

  • We then finally construct the test-object by merging together all properties of defaultTodo with the overwrite object using the spread-operators.

    Like this, all properties given to the function will overwrite the ones in defaultTodo (as the name indicates) while leaving the other properties in place.

Advantages of this pattern

  • It scales: when adding new properties to the original type, you only have to adjust the default-Object in a single place
  • It scales again: If for any reason you need to construct the mock-data differently (e.g. because of deeply nested structures, see below), you are completely free to do so within the factory-function without having to change any callers.
  • Type-safety is on at all times. This prevents you from introdcuing nasty bugs, as well as making refactorings easy, especially with IDEs that support things like renamings (Hit F2 in VSCode ;) ).
  • It's immutable: As long as you don't have nested objects (again, see below on how to handle those), you are guaranteed to get a new copy for every test, preventing another sort of nasty bugs where tests might influence each other.
  • It's documentation: You can give the default-object meaningful values to have a documentation on how it might look like in production.
  • It's reusable: This pattern can be used in a lot of different scenarios - it is actually not even limited to data-objects as you might find out if you read on.

Extending the pattern

The pattern as shown is already useful in a lot of ways. But I promised you scaling, so let me show you how to further extend it for more special use-cases.

Use mock-factories in other mock-factories

Lets say we introduce a User-Type in our App that might look like this:

User.ts

type User = {
  id: string;
  firstName: string;
  lastName: string;
  pronouns: string;
}
Enter fullscreen mode Exit fullscreen mode

We then create a mock-factory for it:

createMockUser.ts

const defaultUser: User = {
  id: '29f51e42-c6ca-4f17-ac93-7131eeb4cffc',
  firstName: 'Kim',
  lastName: 'Su',
  pronouns: 'they/them',
}

const createMockUser = (overwrites: Partial<User> = {}) => ({
  ...defaultUser,
  ...overwrites
});
Enter fullscreen mode Exit fullscreen mode

Now we want to adjust our Todo.assignee-Property to use this type rather than a string:

Todo.ts

type Todo = {
  title: string;
  done: boolean;
  assignee: User;
  dueDate: Date;
  project: string;
  tags: string[];
}
Enter fullscreen mode Exit fullscreen mode

This will break all our tests at first, so we quickly adjust the default-object in the Todo-Factory:

createMockTodo.ts

import { createMockUser } from './createMockUser';

const defaultTodo: Todo = {
  title: 'Default Todo Title',
  done: false,
  assignee: createMockUser(),
  dueDate: new Date('2021-07-04'),
  project: 'Writing Blogposts',
  tags: ['writing', 'blogs']
}

const createMockTodo = (overwrites: Partial<Todo> = {}) => ({
  ...defaultTodo,
  ...overwrites
});

export {
  createMockTodo
};
Enter fullscreen mode Exit fullscreen mode

And that's it. Our tests should work again, given they did not involve or overwrite the user object. And if they did, we can now easily find them through our typechecks (or by following the failed tests for that matter).

For example imagine we had a test like this before the change:

Todo.test.ts

it('changes the assignee to the new given user.', () => {
  const givenTodo = createMockTodo({
    assignee: 'David Losert'
  });

  const { assignee: actualUser } = assignTodo(givenTodo, 'Rahim Vera');

  assert.deepStrictEqual(actualUser, 'Rahim Vera');
});
Enter fullscreen mode Exit fullscreen mode

Now we could write it like this:

it('changes the assignee to the new given user.', () => {
  const givenTodo = createMockTodo({
    assignee: createMockUser({ id: 'oldId' })
  });
  const expectedUser = createMockUser({ id: 'newId' });

  const { assignee: actualUser } = assignTodo(givenTodo, expectedUser);

  assert.deepStrictEqual(actualUser, expectedUser);
});
Enter fullscreen mode Exit fullscreen mode

We have to be careful though: Now that we use nested objects, we are actually able to mutate the values within the defaultTodo:

const myFirstTodo = createMockTodo();
console.log(myFirstTodo.assignee.firstName); 
// Logs 'Kim'

myFirstTodo.assignee.firstName = 'MutatedName';
const mySecondTodo = createMockTodo();
console.log(mySecondTodo.assignee.firstName); 
// Now Logs 'MutatedName'
Enter fullscreen mode Exit fullscreen mode

This is due to the fact that the spread-operator only does a shallow copy of an object, but passes deep nested objects by reference.

This is not too bad if we are actually certain that we are using immutabilty throughout our app. But if not, there is the option of deep cloning.

Use cloneDeep

As there actually is no standard way of deep cloning an object in JavaScript / TypeScript, we'll either have to implement it ourselfs or use a library that does it for us. For the simplicity of this post, I will be using the cloneDeep-function provided by lodash, since it is probably the most popular one.

If we do not want the full lodash-library in our project, we can also do a standalone install of the cloneDeep function and mark it as a dev-dependency (so long as we only use it in our tests):

npm install --save-dev lodash.clonedeep @types/lodash.clonedeep
Enter fullscreen mode Exit fullscreen mode

Please note that you will have to add "esModuleInterop": true in the compilerOptions-section of your tsconfig.json to be able use it.

Now all that is left todo (pun intended) is to adjust the mock-factory accordingly:

createMockTodo.ts

import cloneDeep from 'lodash.clonedeep';

// ...

const createMockTodo = (overwrites: Partial<Todo> = {}) => {
  return cloneDeep({
    ...defaultTodo,
    ...overwrites
  });
};
Enter fullscreen mode Exit fullscreen mode

And that's it. Now you have a truly immutable mock-factory. Note how we did not have to adjust any tests or other areas of the code to make this central change.

Sum up

As shown, the mock-factory-pattern is a big win in scalibility and focus for creating test-data while at the same time being pretty simple. The simplicity makes it reusable for almost every object, and I even use it sometimes to mock IO-Modules like HTTP-Services.

Using the same pattern for all test-data makes writing tests more approchable, and it is especially helpful for newcomers to the project as they can see default implementations of all the relevant data and types.

By having a mix of a default-object and a factory-function, it becomes super flexible while at the same time minimizing maintenance tasks and the need for more static mock code. To put this into perspective: I once was able to delete ten thousands of lines of code from a project simply by introducing this pattern.

And there are still a lot of other ways to use and extend it, but I'll leave it up to you to find and use them.

Link to working example

You can find a working example of the code in my Github repository:

GitHub logo davelosert / mock-factory-pattern

This repository shows the mock-factory-pattern and accompanies my blog-post about it.

Mock-Factory-Pattern in TypeScript

This repository shows a mock-factory-pattern-example in typescript.

The mock-factory-pattern uses a combination of TypeScript's Partial together with a factory function to be able to create scalabe, reusable and focused test data:

const defaultObject: ExampleType = {
  key1: 'value1',
  key2: 'value2'
  // ...
};

const createMockObject = (overwrites: Partial<ExampleType> = {}) => ({
  ...defaultObject,
  ...overwrites
});

export {
  createMockObject
};
Enter fullscreen mode Exit fullscreen mode

You can find two implementations of this pattern here:

You can read more about it in my blog-post which this repository accompanies.

Setup

To execute the tests:

  • Clone this repository and cd into it on your terminal
  • npm install
  • npm test

Top comments (2)

Collapse
 
dopamine0 profile image
Roee Fl

Really good and detailed article. Thank you David.

We're looking into this idea as well for our new unit test infra.

Collapse
 
larbijirari profile image
Larbi Jirari

Hi, Thanks for this awesome article.
do you have a working example with Classes ?
kind regards