How to modularize test doubles
The ever-growing number of files does not spare test files. The number of similar test double files can be used as an indication of a need to refactor or modularize, test doubles. This blog applies the same techniques we used to modularize other layers of a nodejs
application, but in an automated testing context.
In this article we will talk about:
- The need to have test doubles
- How utilities library relates to fixtures library
- Reducing repetitive imports via a unified export library
- How to modularize fixtures of spies
- How to modularize fixtures of mock data
- How to modularize fixtures of fakes
- How to modularize fixtures of stubs
- How to modularize test doubles for reusability
- How to modularize test doubles for composability
Even though this blog post was designed to offer complementary materials to those who bought my Testing
nodejs
Applications book, the content can help any software developer to tuneup working environment. You use this link to buy the book.
Show me the code
var should = require('should');
var expect = require('expect');
var chai = require('chai');
Example:
What can possibly go wrong?
The following points may be a challenge when modularizing test doubles:
- Some testing libraries share dependencies with the project they are supposed to tests
- Individual test doubles can be replicated in multiple places
- With this knowledge, How can we reduce the waste and reuse most of the dependencies?
In the next sections, we make a case on modularization for reusability as a solution to reduce code duplication.
The Status Quo
Every test double library is in fact an independent library. That remains true even when some libraries are bundled and shipped together, as is the case for chai
(ships with should
and expect
). Every mock
, every spy
, and every stub
we make in one place can potentially be replicated to multiple other places that test similar code-blocks, or code-blocks that share dependencies.
One of the solutions to share common test double configurations across multiple test cases is to organize test doubles in modules.
The need to have test doubles in tests.
In these series, there is one blog that discusses the difference between various test doubles: spy/mock/stubs/fake and fixtures. For the sake of brevity, that will not be our concern for the moment. Our concern is to reflect on the why we should have test doubles in the first place.
From the time and cost perspective, It takes longer to load one single file. It would take even longer to load multiple files, be in parallel or sequentially. The higher the number of test cases spanning multiple files, the slower the test runner process will take to complete execution. This adds more execution time, to an already slow process.
If there is one of amongst other improvements that would save us time, reusing the same library quite often while mimicking implementation of other things we don't really need to load(mocking/etc.), would be one of them.
Testing code acts as a state machine, or pure functions, every input results in the same output. Test doubles are essentially tools that can help us save time and cost as a drop-in replacement of expected behaviors.
How utilities relate to fixtures
In this section, we pause a little bit to answer the question: “How utilities library relates to fixtures library”.
Utility libraries(utilities) provide some tools that are not necessarily related to the core business of the program, but necessary to complete a set of tasks. The need to have utilities is not limited to business logic only, but also to testing code. In the context of tests, the utilities are going to be referred to as fixtures. Fixtures can have computations or data that emulates a state under which the program has to be tested.
Grouping imports with unified export library
The module system provided by the nodejs
is a double-edged sword. It presents opportunities to create granular systems, but repetitive imports weakness the performance of the application.
To reduce repetitive imports, we make good use of the index
. This compensates for our rejection to attach modules to the global object. It also makes it possible to abstract away the file structure: one doesn't have to know the whole project's structure to import just one single function.
How to modularize fixtures of spies
The modularization of spies takes one step in general. Since the spies already have a name, It makes sense to group them under the fixture library, by category or feature, and export the resulting module. The use of the index
file makes it possible to export complex file systems via one single import(or export depending on perspective).
How to modularize fixtures of mock data
Mock data is the cornerstone to simulate desired test state when one kind of data is injected into a function/system. Grouping related data under the same umbrella makes sense in most cases. After the fact, it makes sense to manifest data via export
constructs.
How to modularize fixtures of fakes
Fakes are functions similar to implementation they are designed to replace, most of the time third-party functionality, that can be used to simulate original behavior. When two or more fakes share striking similarities, they become good candidates for mergers, refactoring, and modularization.
How to modularize fixtures of stubs
Stubs are most of the time taken as mocks. That is because they tend to operate in similar use cases. A stub is a fake that replaces real implementations, and capable of receiving and producing a pre-determined outcome using mock data. The modularization will take a single step, in case the stub is already named. The last step is to actually export and reveal/expose the function as an independent/exportable function.
How to modularize test doubles for reusability
Test doubles are reusable in nature. There is no difference between designing functions/classes and test doubles for reusability per se. To be able to reuse a class/function, that function has to be exposed to the external world. That is where export
construct comes into the picture.
How to modularize test doubles for composability
The composability on the other side is the ability for one module to be reusable. For that to happen, the main client that is going to be using the library has to be injected into the library, either via a thunk or similar strategy. The following example shows how two or more test doubles can be modularized for composability.
Some Stubbing questions we have to keep in mind – How do Stubbing differ from Mocking – How to Stubbing differs from Spying: Spies/Stubs functions with pre-programmed behavior – How to know if a function has been called with a specific argument?: For example: I want to know the
res.status(401).send()
— more has been discussed in this blog as well: spy/mock/stubs/fake and fixtures
Making chai
, should
and expect
accessible
The approach explained below makes it possible to make pre-configured chai
available in a global context, without attaching chai
explicitly to the global Object.
- There are multiple ways to go with modularization, but the most basic is using exports.
- This technique will not make any library default but is designed to reduce the boilerplate when testing.
var chai = require('chai');
module.exports.chai = chai;
module.exports.should = chai.should;
module.exports.expect = chai.expect;
Example:
Conclusion
Modularization is a key strategy in crafting re-usable composable software. Modularization brings elegance, improves performance, and in this case, re-usability of test doubles across the board.
In this article, we revisited how to test double modularization can be achieved by leveraging the power of module.exports
( or export
in ES7+). The ever-increasing number of similar test double instances make them good candidates to modularize, at the same time makes it is imperative that the modularization has to be minimalistic. That is the reason why we leveraged the index
file to make sure we do not overload already complex architectures. There are additional complimentary materials in the “Testing nodejs
applications” book, on this very same subject.
References
- Testing
nodejs
Applications book - How to add global variables used by all tests in Javascript? StackOverflow Question
- Problem resetting global object. Bug#1 about adding should on global object
- Problem resetting global object. Bug#2 How to make expect/should/assert be global in test files and be able to pass
eslint
- Test doubles ~ Martin Fowler Blog
- Jasmine vs. Mocha, Chai, and Sinon
- Mocking Model Level
tags: #snippets #nodejs #spy #fake #mock #stub #test-doubles #question #discuss