简体   繁体   中英

How can I unit test a function that uses promises and event emitters in Node.js?

My question is about unit testing with promises and event emitters in Node.js. I am using the jasmine framework if that matters.

The code below uses the https module of Node.js to send a request to an API. The API will return JSON. The JSON from the API is the "rawData" variable in the code below.

I want to unit test that the function returns JSON (and not a JavaScript object).

I have unsuccessfully tried several approaches to unit testing that aspect of this function:

1) I tried spying on the Promise constructor so that it would return a fake function which would simply return a JSON string.

2) I have tried spying on the .on('eventType', callback) function of EventEmitters in Node.js to fake a function that returns JSON.

My question is: are either of the two approaches above possible and/or recommend for accomplishing my goal? Is there a different approach to isolating the http request and emitting of events from my unit test objective? Do I need to rewrite this function to facilitate easier unit testing?

 const https = require('https');

 function getJSON() {
  return new Promise((resolve, reject) => {
    const request = https.get(someConfig);
    request.on('response', resolve);
  })
  .then(msg => {
    return new Promise((resolve, reject) => {
      let rawData = '';
      msg.on('data', chunk => { rawData += chunk });
      msg.on('end', () => {
        resolve(rawData);
      });
    });
  })
  .then(json => {
    JSON.parse(json);
    return json;
  })
}

Is there a reason you want to stick to https for making a request? If not, your code and your testing can both become really simple. I'll give an example using axios .

Http request can look like this

getJSON() {
const url = 'https://httpbin.org/get';
return axios
  .get(url)
  .then(response => response);

}

and you can stub the get call with Sinon

 lab.experiment('Fake http call', () => {
  lab.before((done) => {
    Sinon
      .stub(axios, 'get')
      .resolves({ data: { url: 'testUrl' } });
    done();
  });
  lab.test('should return the fake data', (done) => {
    const result = requestHelper.getJSON2();
    result.then((response) => {
      expect(response.data.url).to.eqls('testUrl');
      axios.get.restore();
      done();
    });
  });
});

With the existing code, nock would work like this

lab.experiment('Fake http call with nock', () => {
  lab.test('should return the fake data', (done) => {
    nock('https://httpbin.org')
      .get('/get')
      .reply(200, {
        origin: '1.1.1.1',
        url: 'http://testUrl',
      });
    const result = requestHelper.getJSON2();
    result.then((response) => {
      const result = JSON.parse(response);
      console.log(JSON.parse(response).url);
      expect(result.url).to.eqls('http://testUrl');
      nock.cleanAll();
      done();
    });
  });
});

Full code is here

I would say that you need to refactor the code a little bit to be more testable.

When I write unit tests for functions I keep below points in mind

  1. You do not need to test for the inbuilt or library modules as they are already well tested.

  2. Always refactor your functions to have very specific reponsibility.

Implementing these two in your example, i would separate the server call in a service module whose sole responsibility is to take url (and configurations, if any) make server calls.

Now, when you do that you get two benefits 1. you have a reusable piece of code which you can now use to make other server calls(also makes your code cleaner and shorter)

  1. Since its a module you can now write seperate tests for that module and take the responsibility of checking whether server calls are made from your current module that uses it.

Now all thats left to test in your getJSON function is to spyOn that service module and use tohaveBeenCalledWith and check that data is properly parsed.You can mock the service to return your desired data.

1 its making a service call so test for toHaveBeenCalledWith

2 its parsing to JSON so test for valid/invalid JSON also test for failures

//no need to test whether https is working properly
//its already tested
 const https = require('https');
const service = require("./pathToservice");

 function getJSON() {
  return service.get(somConfig)
  .then(json => {
    JSON.parse(json);
    return json;
  })
}

//its cleaner now
//plus testable

I think you have not succeeded because you're returning directly like that. It should be like:

function getJSON(callback) {
  (new Promise((resolve, reject) => {
    const request = https.get(someConfig);
    request.on('response', resolve);
  }))
  .then(msg => {
    return new Promise((resolve, reject) => {
      let rawData = '';
      msg.on('data', chunk => { rawData += chunk });
      msg.on('end', () => {
        resolve(rawData);
      });
    });
  })
  .then(json => {

        JSON.parse(json);
        callback(json);
      })
    }
   // to use this: 
   getJSON((your_json)=> {
     // handling your json here.
   })

You can use child_process to spawn a test server to provide JSON API. Example:

const { spawn } = require('child_process');
const expect = chai.expect;
const env = Object.assign({}, process.env, { PORT: 5000 });
const child = spawn('node', ['test-api.js'], { env });
child.stdout.on('data', _ => {
 // Make a request to our app
 getJSON((foo)=>{
  // your asserts go here.
  expect(foo).to.be.a('object');
  expect(foo.some_attribute).to.be.a('string')
  // stop the server
  child.kill();
 });
});

You can custom your someConfig variable in test environment to point to ' http://127.0.0.1:5000 '. your test-api.js file is a simple nodejs script that always response an expected JSON for every request.

Updated unit test example

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