How to reduce duplicate code and improve your tests with factory functions
When a test suite grows in size, you often start to see code duplication. This is mainly reflected in the arrange / setup step. One way to avoid this is to take advantage of factory functions. The use of factory functions is a universal pattern that ensures the code remains maintainable and easy to read.
In this blog post, you’ll learn what factory functions are, how they can reduce duplication and improve your tests. The code samples were created with JavaScript, Jest and Vue. Because this pattern is universal, it can be applied with any other language, tool or framework.
The problem
Imagine we have 20 tests, each creating objects by invoking the MasterService
constructor (could be other factory functions instead) like the following
example:
It doesn't look too bad, but if the requirements change and the MasterService
needs additional or less constructor parameters, all the tests will fail and we
have to fix them individually. Besides that, a lot of duplicate arrange / setup
code starts to appear, especially when the parameters of the constructor
increase. This can cause a lot of noise in your tests and violates the famous
DRY (Don't Repeat Yourself)
principle.
What are factory functions?
Before we go to the solution you need to know what factory functions are. A factory function is a design pattern or a common way to solve a problem. There is really nothing difficult about them. They are just functions that creates objects and returns them. Factory functions make it easy to create objects by extracting the logic of object creation into a function. The best way to explain them are by a example:
Note: A common naming convention is that factory functions start with the word create.
Now that we know what factory functions are, we can evolve them further by setting default constructor parameters and overwrite the parameters given by the function. This is very useful when we have to set different parameters for each test:
The solution
Let's take the MasterService.js test suite from the problem chapter and refactor
it with the factory function called createMasterService which we also created
before:
We've reduced the duplicate code and moved the logic of object creation into a factory function. We also set some default parameters ('fakeHttpMock' and 'fakeLoggerMock'). Overriding the default parameters is still possible:
The advantage of this is that the arrange / setup becomes less noisy and easier to read. Also adding or removing additional parameters for each test can be done in one single place. This can make the code more maintainable.
Example with a common front-end framework
In this chapter we go through an example with a common front-end framework
called Vue. As stated before it could be any framework. I choose Vue because it
is known for its simplicity and ease of understanding. Imagine the test below
tests a product component which renders a product name, picture and a formatted
price through something what's called a
filter in Vue. The custom filter:
formatCurrency needs to be in place for each test else Vue throws errors.
Let's take a look at the test suite before the refactor:
Now lets refactor the test suite from before with a factory function called
createWrapper:
Again we've reduced the duplicate code and moved the logic of object creation into a factory function.
The trade-offs
Most things in life come with a cost. The cost of using factory functions is that you increase the abstractions in your code. If you take this pattern to the extreme the tests will instead become more difficult to understand, forcing future developers to spend more time to understand how the code behaves. If a small test stands on its own without any abstractions, it can be easier to understand. Taking this into consideration, repetition in testing isn't always bad. In my experience, using factory functions in large test suites is usually worth the cost of additional abstractions.
Summary
- Use factory functions to get a new instance of an object.
- Reduce boilerplate and duplicate code by extracting common behavior in a factory function.
- Using factory functions for code duplication is a universal pattern.
- Using factory functions can make test code more complex.
- The trade-off in using factory functions is not always worth the benefit of removing duplication.