简体   繁体   中英

Node.js - how to deal with async mocks in test files?

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM