Do you even stub bro?
Covering your code with tests is just like wearing protection - it may be a bother but it will prevent unwanted scenarios.
So, I was writing tests this other day using mockery for mocking my mocks, and came across some stupid feature / bug.
The example code has been dumb-ed down for getting my point across.
The Code follows standard-js syntax.
Problem
Let’s say we had a model called accountant that had a function calculateTaxCredits based on a social insurance number.
Accountant.js
const formulas = require('formulas') // Some library
function calculateTaxCredits (socialNumber) {
return formulas.calculateTaxCredits(socialNumber)
}Notice how it imports a library called formulas.
Let’s unit-test this.
First, enable mockery.
mockery.enable({
warnOnReplace: false, // don't need these warnings
warnOnUnregistered: false // don't need these warnings
})Import the accountant.js module.
const accountant = require('../../../source/models/accountant')Set up test variables.
const testSIN = 123456789
const expectedTaxes = 100Create a Sinon stub to mock the calculateTaxCredits function.
const stubCalculateTaxCredits = sinon.stub()Create the mock for the formulas library.
const formulaMock = {
calculateTaxCredits: stubCalculateTaxCredits
}Then register the mock with formulas library.
mockery.registerMock('formulas', formulaMock)Now, we can run a test to see what happens.
const testSIN = 123456789
const expectedTaxes = 100
...
stubCalculateTaxCredits.returns(expectedTaxes)
const result = accountant.calculateTaxCredits(testSIN)
expect(result).to.equal(expectedTaxes)And don’t forget to disable mockery after.
mockery.disable()Wait, how will I validate that formulaMock will be called instead of the formulas library?
For this post, I’m going to make a modification to the library formulas so that I can clearly know if it’s being called or not.
Formulas Library
function calculateTaxCredits (socialNumber) {
return 0 // DUN DUN DUN
}Now, let’s run our test and see the results.
calculateTaxCredits
should succeed:
AssertionError: expected 0 to equal 100
+ expected - actual
-0
+100Expectation - The mock should return 100.
Reality - Mock was never called.
Here is the full code, for reference.
Accountant.spec.js
describe('calculateTaxCredits', () => {
before(() => {
mockery.enable({
warnOnReplace: false, // don't need these warnings
warnOnUnregistered: false // don't need these warnings
})
})
after(() => {
mockery.disable()
})
it('should succeed', () => {
// Arrange
const accountant = require('../../../source/models/accountant')
const testSIN = 123456789
const expectedTaxes = 100
const stubCalculateTaxCredits = sinon.stub()
const formulaMock = {
calculateTaxCredits: stubCalculateTaxCredits
}
mockery.registerMock('formulas', formulaMock)
// Act
const result = accountant.calculateTaxCredits(testSIN)
// Assert
expect(result).to.equal(expectedTaxes)
})Use a clean cache
So, how do I fix my test?
A quick Google search leads me to this related Stackoverflow post.
Basically, it says enable mockery with useCleanCache set to true.
If you read the documentation, it does indeed sound like a fix.
Basically, Node will cache module exports but using a clean cache will force all the require to load again.
Updated setup
before(() => {
mockery.enable({
warnOnReplace: false, // don't need these warnings
warnOnUnregistered: false, // don't need these warnings
useCleanCache: true
})
})Updated output
calculateTaxCredits
should succeed:
AssertionError: expected 0 to equal 100
+ expected - actual
-0
+100Still doesn’t work. This is expected, as explained below.
registerMock before require
To make it work, you need to put the registerMock before the require calls.
mockery.registerMock('formulas', formulaMock)
// Import module after registering with mockery
const accountant = require('../../../source/models/accountant')Updated output
calculateTaxCredits
√ should succeed (88ms)This works because the accountant module is imported after the mocks have been registered. mockery works via the cache, and if a module is cached before mockery registers anything, it won’t work.
Full test block for reference
describe('calculateTaxCredits', () => {
before(() => {
mockery.enable({
warnOnReplace: false,
warnOnUnregistered: false,
useCleanCache: true
})
})
after(() => {
mockery.disable()
})
it('should succeed', () => {
// Arrange
const testSIN = 123456789
const expectedTaxes = 100
const stubCalculateTaxCredits = sinon.stub().returns(expectedTaxes)
const formulaMock = {
calculateTaxCredits: stubCalculateTaxCredits
}
mockery.registerMock('formulas', formulaMock)
// Require comes after mock
const accountant = require('../../../source/models/accountant')
// Act
const result = accountant.calculateTaxCredits(testSIN)
// Assert
expect(result).to.equal(expectedTaxes)
})
}Seriously though?!
What a bs rule though. My example is simple but this can get real shitty in an actual codebase.
- What if you have previously-imported modules setup inside
beforeor other global blocks? - The error message doesn’t explain why your test is failing. Here is the stack trace from the failed test
The entire log from failing example above
1) calculateTaxCredits
should succeed:
AssertionError: expected 0 to equal 100
+ expected - actual
-0
+100
at Proxy.assertEqual (node_modules\chai\lib\chai\core\assertions.js:1026:12)
at Proxy.methodWrapper (node_modules\chai\lib\chai\utils\addMethod.js:57:25)
at doAsserterAsyncAndAddThen (node_modules\chai-as-promised\lib\chai-as-promised.js:289:22)
at Proxy.<anonymous> (node_modules\chai-as-promised\lib\chai-as-promised.js:255:20)
at Proxy.overwritingMethodWrapper (node_modules\chai\lib\chai\utils\overwriteMethod.js:78:33)
at Assertion.<anonymous> (node_modules\dirty-chai\lib\dirty-chai.js:115:25)
at Assertion.overwritingMethodWrapper (node_modules\chai\lib\chai\utils\overwriteMethod.js:78:33)
at Context.it (test\unitTests\models\accountant.spec.js:55:23)I searched for a fix, and ta-dah~. Here came rewire.
Rewire basically let’s you “wire” a method call to some arbitrary method. Here’s a quick example.
First, you import the module the same way as you do a normal require.
// Path is same as require
const accountant = rewire('../../../source/models/accountant')Create the stub.
// Method stub
const stubCalculateTaxCredits = sinon.stub()Wire the stub to the method calculateTaxCredits.
// Wiring
accountant.__set__('calculateTaxCredits', stubCalculateTaxCredits)And you are done! Whenever accountant.calculateTaxCredits is called, it will call the stub.
stubCalculateTaxCredits.returns(100)
accountant.calculateTaxCredits(testSIN) // 100The entire code for reference
// Path is same as require
const accountant = rewire('../../../source/models/accountant')
// Method stub
const stubCalculateTaxCredits = sinon.stub()
accountant.__set__('calculateTaxCredits', stubCalculateTaxCredits)You can also easily wire multiple functions.
accountant.__set__({
'calculateTaxCredits': stubCalculateTaxCredits
'calculateFee': stubCalculateFee
})
Voila. Now I can write some tests in peace.
On a serious note, I’m not saying mockery is useless. There are scenarios where it’s superior. I’m just outlining a tip for others who may have the same struggle as I did.