How do I unit test HTTP requests?

12,581

Solution 1

I've used supertest in the past and was very happy with it

import server from '../src/server';
import Request from 'supertest';

describe('Server', () => {
  const request = Request(server());

  describe('/api', () => {
    it('Should return a 404 when invalid route', done => {
      request
        .post('/api/notfound')
        .expect(404)
        .end(done);
    });
  });
});

Solution 2

How about mocking http.get with something like that ?

const createHttpGetMock = (expectedStatus) => {
  return httpGetMock = (address) => {
    return new Promise((resolve, reject) => {
      resolve({
        status: expectedStatus,
        headers: {
          // ... headers
        },
        // mock response
      })
    })
  }
}

Then your test could look like this :

describe("Google request", () => {
  it("Resolves when google responds", async () => {
    const httpSuccessMock = createHttpGetMock(200);
    // Inject your mock inside your request function here, using your favorite lib

    const message = await fetchGoogle();
    assert.equals(message, 'Connected to Google');
  })

  it("Rejects when google responds with error", async () => {
    const httpSuccessMock = createHttpGetMock(500);
    // Inject your mock inside your request function here, using your favorite lib

    const message = await fetchGoogle();
    assert.equals(message, 'Failed to connect to Google');
  })
});

This would fulfill the basic contract of a good unit test : regardless of external modules and dependencies, it ensures the module you're currently testing has the correct behaviour in each possible case.

Solution 3

Try Mockttp (disclaimer: I'm the maintainer of Mockttp).

Mockttp gives you a real local HTTP server & proxy that you can easily configure & use for testing, for exactly these kind of cases.

You can either make requests to Mockttp directly, using a localhost URL, or keep making requests to google.com but use Mockttp as a proxy, and then configure whatever responses (or failed connections/timeouts) you'd like to simulate.

There's some examples in the README that show exactly what you're looking for: https://www.npmjs.com/package/mockttp#get-testing. In your specific case, I'd try something like:

const makeRequest = require("./your-code");
const mockServer = require("mockttp").getLocal();

describe("makeRequest", () => {
    beforeEach(() => mockServer.start());
    afterEach(() => mockServer.stop());

    it("resolves happily for successful requests", () => {
        await mockServer.get("http://google.com").thenReply(200, "A mocked response");

        // Here: configure your code to use mockServer.url as a proxy

        let result = await makeRequest();

        expect(response).to.equal('Connected to google');
    });

    it("rejects failed requests with an error", () => {
        await mockServer.get("http://google.com").thenReply(500, "An error!");

        // Here: configure your code to use mockServer.url as a proxy

        let result = makeRequest();

        expect(response).to.be.rejectedWith('Failed to connect to Google');
    });
});

Configuring the proxy will depend on the libraries you're using, but for http.get https://www.npmjs.com/package/global-tunnel should do it, or you can pass a { proxy: mockServer.url } option to the http.get call directly. Or of course you could make the http://google.com URL configurable, set that to mockServer.url, and mock / instead of http://google.com.

The key difference is that this is an real integration test. You're not mocking at the JS level, instead you're sending real HTTP requests, testing what will really be sent and received, and exactly how your full Node + application code will respond to that in practice.

You can mock entirely at the JS level, and your tests will be marginally quicker (in the region of 2ms vs 10ms per test), but it's very easy to get inaccurate results, and have passing tests but broken real-world functionality.

Solution 4

Here is an example of test with mocha and chai. We also need sinon to stub the http library.

http://sinonjs.org/releases/v1.17.7/stubs/

// test.js

const chai = require('chai');
const expect = chai.expect;
const sinon = require('sinon');
const http = require('http');

const someRequest = require('./index');

describe('some request test', function() {
  let httpGetStub;

  beforeEach(function() {
    httpGetStub = sinon.stub(http, 'get'); // stub http so we can change the response
  });

  afterEach(function() {
    httpGetStub.restore();
  });

  it('responses with success message', function() {
    httpGetStub.yields({ headers: { 'content-type': 200 }}); // use yields for callback based function like http.get
    return someRequest().then(res => { 
      expect(res).to.equal('Connected to Google');      
    });
  });

  it('rejects with error message', function() {
    httpGetStub.yields({ headers: { 'content-type': 400 }});
    return someRequest().catch(err => { 
      expect(err.message).to.equal('Failed to connect to Google');      
    });
  });
})

Hope it helps.

Share:
12,581

Related videos on Youtube

BugHunterUK
Author by

BugHunterUK

Updated on September 16, 2022

Comments

  • BugHunterUK
    BugHunterUK over 1 year

    As the title asks how do I test HTTP requests with Mocha and Chai?

    I've recently started learning unit testing and I'm still getting confused with certain aspects of testing. I can get by fine testing methods that return a value but I'm confused on how to test methods that make HTTP/IO requests.

    For example I have the following code:

    module.exports = someRequest => new Promise((resolve, reject) => 
        http.get('http://google.com', resp => {
            if(resp.headers['content-type'] !== 200) {
                reject(new Error('Failed to connect to Google'));
            }
            resolve('Connected to Google');
        })
    );
    

    I want to test 2 cases:

    1. The request to Google succeed
    2. The request to Google failed

    Do I have to mock these requests and if so what's the purpose of mocking a method that is intended to make a HTTP request?

    • aurelienshz
      aurelienshz over 6 years
      How about mocking http.get ?
    • zero298
      zero298 over 6 years
      The purpose is to not be reliant on a network consistency to validate unit tests. The inverse(?) of this is integration testing.
    • aurelienshz
      aurelienshz over 6 years
      Also @BugHunterUK, be careful that resp.headers['content-type'] is not the status of your response. You're probably looking for resp.status there.