Testing immutable js with sinon custom matchers
February 15, 2020
Author: Dave Cohen
Problems with unexpected assertion failures can arise while testing immutablejs
with sinon
. This post is a brief guide on how to create a custom matcher that will correctly calculate the equivalence between a mock/stub/spy call with an immutable
parameter.
The way I’ll be making the assertion is with calledWith
from sinon-chai
(a plugin for chai
that helps with making should
or expect
assertions for sinon
mocks).
This github issue on sinonjs outlines the problem. It’s a very easy one to run into. This post is one solution to it.
Sinon calledWith fails comparing ImmutableJS arguments that it deems equal
Update Feb 20, 2020: This post is now featured on the How To section of the sinonjs site!
The callback and its calling function
Here’s the function in our source code that we’ll be mocking:
import { List } from 'immutable';
function callOptionalImmutableParam(data, { option = new List() } = {}) {
// do some stuff
return 'crunched data';
}
The interesting thing about this function is that it takes an optional argument with named parameter option
that will default to an empty Immutable List.
Some theoreticalFunction
could use this as a callback:
function theoreticalFunction(data, callback) {
// pretend we're dynamically getting a List from `data`
const customList = new List(['custom', 'list']);
callback(data, { option: customList });
}
Successfully testing the callback
To test that theoreticalFunction
calls callOptionalImmutableParam
with the right arguments, here’s the setup:
import { expect } from 'chai';
import sinon from 'sinon';
import { List, is } from 'immutable';
/**
* Get a custom matcher to validate the option List
* @param {Immutable.List} expected option List
* @returns {Function} sinon.match
*/
function getMatcher(expected) {
return sinon.match(
// use Immutable.is instead of deepEquals
value => is(expected, value.option),
// message if path doesn't match:
JSON.stringify(expected.toJS())
);
}
The getMatcher
function will be used in our assertions. It takes an expected
value (which should be Immutable) and runs it through a sinon.match
custom matcher function. The comment // use Immutable.is instead of deepEquals
points out what’s great about the custom matcher - we can use Immutable’s built-in comparison to get at the values inside of any Immutable data object. We also stringify
the passed in expected
argument so that if our assertion fails, we get a useful message instead of undefined
.
And the assertion:
it('calls callback with expected arguments', function() {
const mockCall = sinon.stub();
// call the function with `mockCall` as the callback:
theoreticalFunction({ data: 'data' }, mockCall);
// use `getMatcher` as the 2nd parameter of `calledWith`
expect(mockCall).to.have.been.calledWith(
{ data: 'data' },
getMatcher(new List(['custom', 'list']))
);
});
We use getMatcher
inside of calledWith
(along with the first data
argument) which gives us a passing test!
Testing without the matcher
Suppose we didn’t bother with a custom matcher:
it('calls callback with expected arguments', function() {
const mockCall = sinon.stub();
theoreticalFunction({ data: 'data' }, mockCall);
expect(mockCall).to.have.been.calledWith(
{ data: 'data' },
{ option: new List(['custom', 'list']) }
);
});
chai
’s built-in deep equals check is a no-go in this case. It will compare the (practically) useless meta-data inside the Immutable List instance:
AssertionError: expected { path: List [ "custom", "list" ] } to equal { path: List [ "custom", "list" ] }
+ expected - actual
{
"path": {
- "__altered": true
+ "__altered": false
"__hash": [undefined]
"__ownerID": [undefined]
"_capacity": 2
"_level": 5
"array": [
"moments"
0
]
- "ownerID": {}
+ "ownerID": [undefined]
}
"size": 2
}
}
While the values in the Immutable structure are equivalent, the deep equals check finds these hidden properties. The failures are prefixed with -
.
The test failure terminal output can also be a list of all the calls to the mock function. One of them will look exactly correct, but will still complain about not being a match.
Read more
To learn more about sinon matchers and the related topics, have a look at the links below.