Currently i am learning how to test node modules. In the recent days i have asked a couple of questions here on StackOverflow about how to mock node modules in order to test, for example, what happens in the .then() clause of a promise. I got some great suggestions from the community on how to tackle this and i have come quite far. However there is still something that i can't wrap my mind around and it has to do with asynchronous calls.
For example, i currently have the following code to add a post:
const makeRequestStructure = require('./modules/makeRequestStructure.js').makeRequestStructure
const normalizeFinalResponse = require('./modules/normalizeFinalResponse.js').normalizeFinalResponse
const doARequest = require('./modules/doARequest.js').doARequest
exports.addPost = (event) => {
const requestStructure = makeRequestStructure('POST', '/posts')
const requestPostData = {
title: event.body.title,
content: event.body.content
}
return doARequest(requestStructure, requestPostData).then((res) => {
const finalResponse = normalizeFinalResponse(200, res)
return finalResponse
}).catch((err) => {
const finalResponse = normalizeFinalResponse(400, err)
return finalResponse
})
}
The helper functions that are required in order to run this file are:
makeRequestStructure.js
(located at ./modules/makeRequestStructure.js
)
require('dotenv').config()
const { HOST, PORT } = process.env
module.exports.makeRequestStructure = function (method, path) {
return {
host: HOST,
port: PORT,
method: method,
path: path
}
}
This module uses environment variables, the ones that i have configured in my .env file are
HOST=jsonplaceholder.typicode.com
POST=433
Next i have the normalizeFinalResponse.js
file and the doARequest.js
files:
normalizeFinalResponse.js
(located at ./modules/normalizeFinalResponse.js
)
module.exports.normalizeFinalResponse = function (statusCode, message) {
return {
'statusCode': statusCode,
'body': { message: message }
}
}
doARequest.js
(located at ./modules/doARequest.js
)
const https = require('https')
module.exports.doARequest = function (params, postData) {
return new Promise((resolve, reject) => {
const req = https.request(params, (res) => {
let body = []
res.on('data', (chunk) => {
body.push(chunk)
})
res.on('end', () => {
try {
body = JSON.parse(Buffer.concat(body).toString())
} catch (e) {
reject(e)
}
resolve(body)
})
})
req.on('error', (err) => {
reject(err)
})
if (postData) {
req.write(JSON.stringify(postData))
}
req.end()
})
}
Now, this code is pretty straightforward. By running the following file it will make a POST call to jsonplaceholder.typicode.com:433/posts
with in its body { body: { title: 'Lorem ipsum', content: 'Lorem ipsum dolor sit amet' } }
const addPost = require('./addPost.js').addPost;
const event = { body: { title: 'Lorem ipsum', content: 'Lorem ipsum dolor sit amet' } }
addPost(event).then((res) => {
console.log(res);
}).catch((err) => {
console.log(err);
});
In the addPost
module the function normalizeFinalResponse
is called to normalize the response from the jsonplaceholder api. In order to check this i have created the following test file.
//Dependencies
const mock = require('mock-require')
const sinon = require('sinon')
const expect = require('chai').expect
//Helper modules
const normalizeFinalResponse = require('../modules/normalizeFinalResponse.js')
const doARequest = require('../modules/doARequest.js')
//Module to test
const addPost = require('../addPost.js')
//Mocks
const addPostReturnMock = { id: 101 }
describe('the addPost API call', () => {
it('Calls the necessary methods', () => {
console.log(1)
//Mock doARequest so that it returns a promise with fake data.
//This seems to be running async. The test file continues to run when its not resolved yet
mock('../modules/doARequest', { doARequest: function() {
console.log(2)
return Promise.resolve(addPostReturnMock);
}});
console.log(3)
//Stub functions expected to be called
let normalizeFinalResponseShouldBeCalled = sinon.spy(normalizeFinalResponse, 'normalizeFinalResponse');
//Set a fake eventBody
let event = { body: { title: 'Lorem ipsum', content: 'Lorem ipsum dolor sit amet' } }
//Call the method we want to test and run assertions
return addPost.addPost(event).then((res) => {
expect(res.statusCode).to.eql(200);
sinon.assert.calledOnce(normalizeFinalResponseShouldBeCalled);
})
});
});
Running this test file fails the assertion because apparently the normalizeFinalResponse
function is never called. When i use the console.log's they print in the order 1,3,2. This leads me to belive that the mock()
function is not finished yet and so it will make an actual call to the jsonplaceholder api. But than still the function normalizeFinalResponse
should have been called, right?
I have the feeling that i overlook something thats right in front of my eyes. However i can't figure out what it is. If you know what is wrong with my test i would love to hear it. It helps me understand writing this kind of tests better.
My package.json for reference:
{
"name": "mock-requests-tests",
"version": "0.0.1",
"description": "A test repository so i can learn how to mock requests",
"scripts": {
"test": "mocha --recursive tests/",
"test:watch": "mocha --recursive --watch tests/"
},
"devDependencies": {
"chai": "^4.1.2",
"mock-require": "^3.0.2",
"sinon": "^7.2.2"
}
}
The reason the normalizeFinalResponse
spy is never called is that addPost
is calling the method directly and not using the spy that was created locally.
When you call sinon.spy()
is creates a pass-thru function that tracks if this new method was executed. This is by design, as you wouldn't want code constantly changing your functions out from under you.
Generally speaking, you would not care if normalizefinalResposne
had executed at all. The only thing that you should be concerned with is that given input addPost(x)
it returns y
the internal details do not matter so long as for input x
you get result y
. This also simplifies refactoring later as your unit test will only break if the functionality stops working as expected, not just because the code looks different while the functionality remains the same.
There are some exceptions to this rule, mostly when using 3rd party code. One such example is in doARequest
. You may want to use a mock library to override the http
module so that it returns crafted responses and does not make live network requests.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.