Let's say we have a small Node program and want to confirm it logs "Hello" string to the console
1 | const app = () => console.log('Hello') |
1 | $ npm start |
We need a test runner to write a test and an ability to spy console.log
method. We could write both the test runner and method spying ourselves, but there are already 2 great tools for this: Mocha.js and Sinon.js. Let's use them.
Note: you can find the source code for this blog post in mocha-sinon-example repository.
Basic setup
First, install both NPM dependencies
1 | $ yarn add -D mocha sinon |
Next, write a spec file test/app-spec.js
that will import app
, set up spying and then confirm the spy was called with expected argument
1 | const sinon = require('sinon') |
To run unit tests, I like using NPM script command npm test
1 | { |
The test passes
1 | $ npm t |
Tip: whenever there is new code pulled for the project, I like using shortcut npm it
to run npm install
+ npm test
together. Even better, I could use shortcut npm cit
that runs npm ci
+ npm test
together.
Sandbox
We have set up a Sinon spy on the global console.log
- but we have not cleared it. This could lead to unexpected behavior in the unit tests that follow. For example if the next test runs by itself it works
1 | it('logs Hello', () => { |
But when the two tests run together the second attempt to spy causes an error.
1 | 1) logs again: |
We really don't want the two tests to be dependent on each other. Thus I strongly recommend using Sinon sandboxes. We could create the sandbox once and reset the sandbox before each test in a single call.
1 | const sinon = require('sinon') |
Global sandbox
Rather than each spec file creating a sandbox (and restoring it), we could move the before
and beforeEach
hooks into its own test helper file, creating root-level hooks.
1 | const sinon = require('sinon') |
1 | const { app } = require('../app') |
I change the NPM test script to load the helper file
1 | { |
CI setup
Let's run these tests on CI - I will use GitHub Actions because they are awesome. I have added .github/workflows/test.yml
file.
1 | name: main |
The code is checked out, bahmutov/npm-install action runs yarn
with caching of NPM dependencies, and then npm t
runs unit tests.
Assertions
In the above example, we used a plain Error in case the spy was not called as expected.
1 | it('logs Hello', () => { |
Sinon.js comes with assert
methods that can help produce better error messages with more context. For example, if we expect the spy to have been called with "Bye" for some reason, the error message prints the actual calls
1 | it('logs again', () => { |
Even better is to bring in Chai assertions with Sinon.js Chai plugin.
1 | $ yarn add -D chai sinon-chai |
Then add to the test/helper.js
file the following
1 | const chai = require('chai') |
Now we can use easy to read assertions inside the specs
1 | it('logs again', () => { |
Matches
If you don't know the exact argument a stub or a spy should be called, you can use sinon.match
utils. For example, if the code calls the method with o.method('hello', name)
then you can assert it with
1 | const sinon = require('sinon') |