Table of contents
- A guide to test double your Node.js code
(Just a fake cover generated on O RLY Cover Generator, don't search the book on amazon)
This guide helps you to answer the following questions:
Should I use a spy or a stub?
In real life both useful types of mocks are useful, but for different purposes.
- Spies to keep the behavior of your dependency and track the usage.
- stubs to replace the behavior of your dependency in order to check your system under test.
Should I use 'module interception'?
Module interception is the way to make full test double.
- For spy you simply can't use 'module interception', use a simple stubbing tool.
- For stubs the best practice it to use module interception, but you can do without.
Which library should I use?
This matrix should help you to make your choice:
Specificities of each library
My preferred library stays Jest:
- It's integrated
- You can use it with or without module interception
- There are no surprises.Can I have a basic example for each library?
Pick the sample code you need:
- Jasmine
- Jest no interception
- Mocha + Chai + Sinon
- Jest with interception
- Mocha + Chai + Sinon + proxyquire
- Mocha + Chai + Sinon + rewire
- Mocha + Chai + Sinon + rewiremock
- Mocha + Chai + testdouble
Now if you want more details, let's start...
My goal in this guide is to go from theory to practice about tests double in Node.JS. I'll try to cover those questions:
- Should I use a spy or a stub?
- What is module interception?
- Which javascript library should I use?
Let's go back to the basics...
Definition of Test doubles (from Wikipedia): In automated unit testing, it may be necessary to use objects or procedures that look and behave like their release-intended counterparts, but are actually simplified versions that reduce the complexity and facilitate testing. A test double is a generic (meta) term used for these objects or procedures.
Mock is sometimes used to refer to all types of test doubles, but in fact mock is just one type of test double. This is why I'll try to use 'test double' and not 'mock' in this guide (as a noun and as a verb).
| Types of test doubles | intended to be used |
can get usage info |
implementation replacement |
implementation contains checks |
|---|---|---|---|---|
| Dummy | - | - | - | - |
| Spy | ✔️ | ✔️ | - | - |
| Stub | ✔️ | ✔️ | static | - |
| Fake | ✔️ | ✔️ | complex | - |
| Mock | ✔️ | ✔️ | ✔️ | ✔️ |
Test doubles is a general term to refer to different types of objects. This list describes 5 of the most used by order of complexity:
- Dummy: simple implementation of an interface. It's not intended to be used by your system under test.
- Spy: (or Test Spy) get information on dependency usage without changing the behavior. (Number of calls, arguments)
- Stub: (= Dummy + static implementation) test double with modification of the behavior in order to test your component.
- Fakes: (= stub + implementation) A stub that implements some business logic. e.g. A simulator is a kind of complex fake.
- Mock: (= stub + internal test) test double which is aware of the test (with some test assertion for example).
Be careful: Mock as this type of specific type of test double built specifically for your test is in fact considered as an anti-pattern most of the time. It breaks the AAA (Arrange Act Assert) test structure. You should probably consider replacing it with a stub or a fake.
| Types of test doubles | in Node.js real life use |
|---|---|
| Dummy | Nothing, it's useless |
| Spy | Spy |
| Stub | Stub |
| Fake | Stub |
| Mock | Don't use them, prefer Stub |
Dummy is not really useful in javascript, because there is no need to implement any fixed interface. Therefore you'll never have to implement test doubles if it's not intended to be used in your component under test.
Spy is non invasive test double provided by almost all the testing frameworks. You could technically implement a spy yourself but it's really not worth it.
Stub is invasive test double provided by almost all the testing frameworks. You could technically implement a stub yourself but it's really not worth it.
Fake is just smart stub, their implementation is smarter and fully functional in contrast to a stub which has a very basic implementation (static most of the time). We'll consider fake and stub as one category.
Mock is just stub with some awareness of your test. Mock is not the first type of test double to consider. Sinon have special objects for this, on other frameworks you need to use stub.
Test doubles in javascript can be performed at 2 different levels: Partial test double and full test double.
With Partial test double you are just replacing a small part of your real dependency. For doing so you need a simple library named 'Subbing library'. Sinon is the most well known library to only do subbing.
With Full test double you are replacing the full javascript module with your own version for testing, not just a small part. For doing so you need a more complex library named 'module interception library'.
This is the only way to get a real spy, a spy that has the original behavior of your dependency. Module interception will not be able to give you a real spy.
If you need a stub, you can get one with a simple Subbing library but it's considered as an anti-pattern (for stubs prefer Full test double). Explanation from Justin Searls the creator of testdouble
Step by step:
- Import your component under test (of course)
- Import the component dependency
- Replace each method dependency with your spy or stub method.
For this you just need a test doubles library. It's named partial test double because you are keeping the behavior of your entire dependency except the part (e.g. function) you want to spy or stub.
This is the best way to stub your dependency in a clean way. To make a full stub you need a javascript module interception library.
Step by step: The way to intercept the module dependency in your component under test will not be the same depending on the library but the only common point is that you SHOULD NOT import the component dependency directly.
See example with:
It's named full test double because you are not keeping anything of your original dependency behavior.
Spying in a full test double is not possible, because by intercepting the javascript dependent module you are not importing the original module at all. You have to stub each method of your dependency used by your system under test. If you forget to stub one, the module interception library should throw an exception. (It's not the case for all libraries)
For module interception, you should consider the type of import (CommonJS, ES6 default import, ES6 named import). CommonJS is the easier to use than ES6 Modules. Making full stub in ES6 can be tricky. See this article for more explanation: Jest Full and Partial Mock/Spy of CommonJS and ES6 Module Imports
There are plenty of test libraries in javascript for different purposes. Some are made to be used together some not. For the purpose of Test Doubles we'll be interested in only these types of tools: stubbing library and Module interception library.
To perform your 'test double' tests, you'll need these 4 features: a test runner, an assertion library, a test double creator and a module interceptor. If you just want to create spies, you just need the 3 firsts ones. If you want to create a stub, you need all.
| Library / purpose | Test runner | Assertion Lib | Test double creator | Module interceptor |
|---|---|---|---|---|
| jest | ✔️ | ✔️ | ✔️ | ✔️ |
| jasmine | ✔️ | ✔️ | ✔️ | - |
| mocha | ✔️ | - | - | - |
| chai | - | ✔️ | - | - |
| should.js | - | ✔️ | - | - |
| expect.js | - | ✔️ | - | - |
| better-assert | - | ✔️ | - | - |
| sinon | - | - | ✔️ | - |
| testdouble | - | - | ✔️ | ✔️ |
| proxyquire | - | - | - | ✔️ |
| rewire | - | - | - | ✔️ |
| mock-require | - | - | - | ✔️ |
| rewiremock | - | - | - | ✔️ |
Some tools are like a swiss army knife for tests (like Jest) doing a lot of different tasks so you'll find them in multiple categories. There are also some compatibility issues between tools and platform (ES and CommonJS).
Let's define each feature...
Test runner: The test runner find tests in your code, launch test, generate and display test progress and results. The main ones are: Jest, Mocha, Jasmine.
Assertion Lib: The assertion helps to check the test results. It's already included in Jest and Jasmine. If you don't use these libraries, you can pick mocha as test runner and chai or should.js, expect.js, better-assert.The most popular stacks are Jest or mocha+chai.
Test doubles creator: We are arriving at our main subject: test doubles. In this section we are only talking about the way to provide spies and stubs. Full test doubles are often used with javascript module interception but it's an add-on. The mains libraries are: Jest, Sinon, Jasmine, Testdouble (the library, not the concept).
Module interception libraries: This type of library will help you to replace a module dependency in your javascript. Each one has a very different way of doing it. Module interception is sometimes named: 'Dependency mocking', 'overriding dependencies during testing', 'mocking of Node.js modules', 'mock require statements in Node.js'.
Tools: The main ones are: Proxyquire, Rewire, Mock-require, Testdouble, Rewiremock.
In order to understand all the different combination of libraries and how to use them together, I have created the same basic example with 8 different stacks.
| Stack tested in this project / features | Test runner | Assertion | Test double | Module interception |
|---|---|---|---|---|
| 1. Jasmine | ✔️ | ✔️ | ✔️ | - |
| 2. Jest no interception | ✔️ | ✔️ | ✔️ | - |
| 3. Mocha + Chai + Sinon | ✔️ | ✔️ | ✔️ | - |
| 4. Jest with interception | ✔️ | ✔️ | ✔️ | ✔️ |
| 5. Mocha + Chai + Sinon + proxyquire | ✔️ | ✔️ | ✔️ | ✔️ |
| 6. Mocha + Chai + Sinon + rewire | ✔️ | ✔️ | ✔️ | ✔️ |
| 7. Mocha + Chai + Sinon + rewiremock | ✔️ | ✔️ | ✔️ | ✔️ |
| 8. Mocha + Chai + testdouble | ✔️ | ✔️ | ✔️ | ✔️ |
Each solution will test the following basic code. The goal is to test the module A (The system under test) which reference the module B (the dependency to test double). We'll do it with spy and stub with partial or full test double.
ModuleB.js = the dependency to test double
function DoItB() {
return "B";
}
module.exports = { DoItB };ModuleA.js = system under test
var moduleB = require("./moduleB");
function DoItA() {
return "A(" + moduleB.DoItB() + ")";
}
module.exports = { DoItA };Basic syntax of spy, stub and mock in different libraries:
| Tool | spy | stub | mock |
|---|---|---|---|
| Sinon | sinon.spy() | sinon.stub() | sinon.mock() |
| Jest | jest.spyOn() | jest.fn() | no / use stub |
| Jasmine | jasmine.spyOn() | jasmine.spyOn() | no / use stub |
| testdouble | td.func() | td.func() | no / use stub |
Let's now look at some implementation details about how each library deals with some specific requirements.
| Tool | Module interception | Spy implementation | Siblings method call | Dependency Path |
|---|---|---|---|---|
| 1. Jasmine | NO | 😕 FAKE | SAME | r/test |
| 2. 💕 Jest no interception | NO | SAME | SAME | r/test |
| 3. Mocha + Chai + Sinon | NO | SAME | SAME | r/test |
| 4. 💕 Jest with interception | YES | FAKE | ERROR | r/test |
| 5. Mocha + Chai + Sinon + proxyquire | YES | FAKE | 😵 SAME | 👎 r/sut |
| 6. Mocha + Chai + Sinon + rewire | YES | FAKE | ERROR | 👎 VarName |
| 7. Mocha + Chai + Sinon + rewiremock | YES | FAKE | ERROR | r/test |
| 8. Mocha + Chai + testdouble | YES | FAKE | 😵 EMPTY | r/test |
(Tested versions: mocha=6.2.2, chai=4.2.0, sinon=7.5.0, jasmine=3.5.0, jest=24.9.0, proxyquire=2.1.3, rewire=4.0.1, rewiremock=3.13.9, testdouble=3.12.4)
Let's explain the meaning of each column.
Column 'Module interception'
You have a proper module interception library when you don't have to import the original dependency in your test. But in fact there are multiple ways to do module interception and each library is doing this differently.
Column 'Spy implementation'
Is the behavior of the original dependency staying the same? If the answer is yes, it's a real spy. If not, it's a fake spy, it's just an empty stub returning undefined. You can't expect any Module interception library to keep the behavior of the original dependency because by nature, Module interception will NEVER use your original dependency at all.
Column 'Siblings method call'
Once you have stubbed a method in your dependency, The question is to understand if calling a sibling method has the expected behavior. You can expect 3 types of behavior:
- SAME: The original behavior of the sibling method is not modified, It's generally what people call a partial test double.
- EMPTY The sibling method return undefined. It's a weird mix between a partial and a full test double.
- ERROR The sibling method throws an exception "the method doesn't exist". It's what you can expect from a full test double. It's probably the best option because you are sure that no other method of your dependency is used.
Exceptions:
- Proxyquire is supposed to be a module interception library (for full test double) but the sibling method will stay unchanged!
- Testdouble is supposed to be a module interception library (for full test double) but the sibling method will still exist with an undefined behavior!
Column 'Dependency Path'
In your test code you'll have to specify the path to the dependency you want to test double. You can expect 3 types of behavior:
- r/test The path is relative to the test file. It's the best option.
- r/sut The path is relative to the module under test. It's a bad idea! In some cases it will be tricky.
- VarName The name of the variable in the system under test. It's a bad idea!
Read the TL;DR: for the summary. My personal choice is Jest, easy, versatile and trustworthy.
Understanding Test Doubles (Mock vs Stub)
mocking ES and commonJS modules with jest mock
Please stop playing with proxyquire Please stop playing with proxyquire by the creator of rewiremock
An Overview of JavaScript Testing in 2019
Jest vs Mocha: Which Should You Choose?
Jasmine vs. Mocha, Chai, and Sinon
Jest Full and Partial Mock/Spy of CommonJS and ES6 Module Imports
