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);
})
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);
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:
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.
-
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 typeas Thing
everywhere which again is quite a bit of a distraction in my eyes. -
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 thedefaultTodo
-object. And we get another bonus: thedefaultTodo
serves as a great documentation on how a realTodo
-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
};
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);
}
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 anotherType
orInterface
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 inTodo
. 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 theoverwrite
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;
}
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
});
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[];
}
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
};
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');
});
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);
});
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'
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
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
});
};
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:
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
};
You can find two implementations of this pattern here:
- src/createMockUser.ts: simple example
-
src/createMockTodo.ts: example using
cloneDeep
to ensure immutability.
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 (3)
Good job, your article is evergreen and I like the clean and simple solution!
Really good and detailed article. Thank you David.
We're looking into this idea as well for our new unit test infra.
Hi, Thanks for this awesome article.
do you have a working example with Classes ?
kind regards