Optimize Jest fixtures using dependency injection. Inject environment functions to streamline code, ease test creation.
The problem to solve
In previous lesson - ⭐ Mocking environment variables, we created a nice working fixture that reduces lines of code when we write tests.
However, the result is not "ideal". Still, we may reduce and remove some responsibilities from developers. Check the following code to understand what I mean:
So, imagine a situation when you forget to add afterEach and fixture.restore() statement in one of your tests... It may happen because the fixture.restore() call cleans up all mocks in the environment object. Yea, it will be pretty hard to spot what is causing false positives or false negatives in our tests.
Good news! Dependency injection pattern will fix all our problems 💨.
The dependency injection pattern
Let's explain it with another example not connected with our problem to understand the basic idea. Imagine that you've installed a library called "users-list" which is a React component with sorting capabilities (yea, really complicated :P).
The implementation of component looks like this (remember it's a library code - in your app you just installed it):
Now, imagine that the implementation of this component sorting mechanism is broken - looks like the sort function from Lodash has a bug!
So, what's now? What kind of options do you have? You can try to find another lib, copy the lib source code to your code, wait for a fix, or implement it completely on your own... All of these options are time-consuming and it's just a sort function bug...
If the author of the library would add one small change, we would not have to do anything... Check the code below to understand how we may improve it:
Instead of relying on and being coupled with Lodash without the option to override sort mechanism, we've added UsersListProps interface, to be able to change sorting mechanism. That's how dependency injection pattern works in its simplest form.
You need to pass a function that implements the required contract - in our case specified via TypeScript interface. Now, we still are coupled with Lodash, but we may replace it from consumer code, not inside UsersList library implementation (in addition we improved our code from the SOLID perspective - the Open/Close principle.
The last change must be implemented in the consumer component and our little bug will be fixed without any additional effort.
Dependency injection pattern may be implemented in many different ways. We may use injection tokens and more advanced mechanisms. The presented approach is the simplest implementation of it!
Fixing our fixture with dependency injection
We'll use the same technique as above. We'll pass afterEach function from Jest to our fixture. When the fixture is called the first time, it will trigger afterEach, so in test files all of these statements will be removed.
First, we added an additional type to define the signature of afterEach function. We used higher order function pattern and closure mechanism to be able to call our fixture function twice.
Second, with injected afterEach and passed the key that is required. Thanks to the closure, afterEach callback will be remembered and we'll be able to do stuff like this:
If you would like to add additional logic to afterEach and you're using this fixture - don't worry about anything, the Jest will run as many callbacks as you define :).
So, you will have always called all defined afterEach functions, the one that we're passing and the others defined in tests. Thanks to that you're not limited to only one afterEach call.
The final result
Conclusions and thoughts
As you saw we've added a nice improvement for developers that reduces the code that they need to write and the responsibility to "remember". In this particular fixture, it was just 3 lines of code and one responsibility, but in more advanced ones it may be much more!
To see more advanced fixtures you may read ⭐ Creating fixture for MSW to reduce boilerplate and setup.
- 1. Basics
Grouping the tests
The usage of describe and it
The best practices for naming tests
Navigating the different types of software tests
- 2. Mastering unit testing
Project and tests setup
Unit tests review
React component testing
Snapshot testing in React
Understanding stubs in testing
Understanding mocks in testing
Creating testing fixtures
Using spies in React and Typescript
Mocking environment variables
Using dependency injection pattern to improve fixtures
- 3. Mastering integration testing
Understanding the integration tests
Using MSW library to remove implementation details from tests
Creating fixture for MSW to reduce boilerplate and setup
- 4. Mastering e2e tests