3

I'm trying to test a function that gets data from an external API using axios. To keep my test function as close as possible to the real thing, I'm querying mock data I have in a file. Axios can't return data from local files, which is a security feature. So the solution I'm trying is spinning up a simple server in my test suite, serving the file there, and then running my tests.

My test suite looks like this right now:

import React from 'react';
import {shallow} from 'enzyme';
import express from 'express';
import { getFeedId, getFeedData, reverseStop } from '../mocks/apiMock';

const app = express();
const port = 4000;
app.use(express.static('../mocks/MockData.json'));
app.listen(port, tests());

function tests () {
    it('returns the expected feed id for a given subway line', () => {
        expect(getFeedId('L')).toBe(2);
    });

    it('returns json data', () => {
        expect.assertions(2);
        return getFeedData('L').then(data => {
            expect(data).toBeDefined();
            expect(data.header.gtfs_realtime_version).toBe('1.0');
        });
    });

    it('returns a stop_id for a given subway line and stop', () => {
        expect(reverseStop('L', 'Lorimer St')).toBe('L10N');
    });
}

The functions I'm testing look like this (the one that uses Axios is getFeedData, so I don't think the others are causing a problem but I'm not positive).

const axios = require('axios');

export function getFeedId (sub) {
    switch (sub) {
        case '1': case '2': case '3': case '4': case '5': case '6': case 'S':
            return 1;
        case 'A': case 'C': case 'E':
            return 26;
        case 'N': case 'Q': case 'R': case 'W':
            return 16;
        case 'B': case 'D': case 'F': case 'M':
            return 21;
        case 'L':
            return 2;
        case 'G':
            return 31;
    }
}

export function getFeedData (sub) {
    if (getFeedId(sub) === 2) {
        return axios.get('http://localhost:4000').then((data) => JSON.parse(data));
    }
}

export function reverseStop (sub, stop) {
    const stops = require('../utils/stops');
    const stopObjs = stops.filter((item) => item.stop_name == stop && typeof item.stop_id === 'string' && item.stop_id.charAt(0) == sub);
    for (var i = 0; i < stopObjs.length; i++) {
        if (stopObjs[i].stop_id.charAt(stopObjs[i].stop_id.length - 1) == 'N') {
            return stopObjs[i].stop_id;
        }
    }
}

Here's the error message Jest is giving me:

FAIL  src/tests/api.test.js (23.311s)
  ● returns json data

Network Error

  at createError (node_modules/axios/lib/core/createError.js:16:15)
  at XMLHttpRequest.handleError [as onerror] (node_modules/axios/lib/adapters/xhr.js:87:14)
  at XMLHttpRequest.callback.(anonymous function) (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:289:32)
  at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:219:27)
  at invokeInlineListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:166:7)
  at EventTargetImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:122:7)
  at EventTargetImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:87:17)
  at XMLHttpRequest.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:61:35)
  at dispatchError (node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js:994:9)
  at validCORSHeaders (node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js:1009:7)
  at receiveResponse (node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js:871:12)
  at Request.client.on.res (node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js:691:38)
  at emitOne (events.js:96:13)
  at Request.emit (events.js:191:7)
  at Request.onRequestResponse (node_modules/request/request.js:1074:10)
  at emitOne (events.js:96:13)
  at ClientRequest.emit (events.js:191:7)
  at HTTPParser.parserOnIncomingClient (_http_client.js:522:21)
  at HTTPParser.parserOnHeadersComplete (_http_common.js:99:23)
  at Socket.socketOnData (_http_client.js:411:20)
  at emitOne (events.js:96:13)
  at Socket.emit (events.js:191:7)
  at readableAddChunk (_stream_readable.js:178:18)
  at Socket.Readable.push (_stream_readable.js:136:10)
  at TCP.onread (net.js:560:20)

  ● returns json data

expect.assertions(2)

Expected two assertions to be called but only received zero assertion calls.

  at addAssertionErrors (node_modules/jest-jasmine2/build/setup-jest-globals.js:68:21)
  at process._tickCallback (internal/process/next_tick.js:109:7)```

My best guess at the issue is that maybe Jest doesn't run in a node environment (is there any way I can figure that out)? So maybe the Express server isn't being run at all. However, I'm a little beyond my expertise so that is little more than a guess. Is anyone able to shed a little light on what's really going on? Was my idea to run the Express server a good one? Was it a good idea to put it in the test suite? If the answer to one or both of those questions is "no," what are best practices here?

bkula
  • 541
  • 3
  • 10
  • 22
  • If it wasn't running in Node, Express itself would give errors. – SLaks Dec 04 '17 at 22:42
  • Side note: You aren't actually passing a callback to `listen()`. Passing one might solve your problem. – SLaks Dec 04 '17 at 23:13
  • I'm not passing a callback to listen? I thought passing `tests()` as the second parameter was my callback. Also, you say below to make `beforeEach()` async and pass the callback to `listen()`. Is `listen()` not supposed to be included in `beforeEach()`? – bkula Dec 04 '17 at 23:18
  • No; you're _calling_ `tests()` and passing its return value (which is `undefined`). Just like any other function call. – SLaks Dec 05 '17 at 00:59
  • You should call `listen()` in `beforeEach()` and pass it the `done` callback from `beforeEach()`. Read the documentation & learn how callbacks work. – SLaks Dec 05 '17 at 01:00
  • @bkula - You deleted your other question from today so we can't respond to the comment you left there. One down vote should not scare you off. That was probably because the question is lacking a lot of information needed to help. Folks would likely help you, but you have to explain what you're trying to accomplish in words before we know how. Your first attempt at code was too far off for us to know what you were trying to accomplish, thus you need to use words to explain a mini-spec for what you're trying to do and show what you tried and what went wrong with that attempt. – jfriend00 Dec 20 '17 at 03:30

3 Answers3

2

To avoid code duplication between all your source files, you can create a node environment, which will be setup for all your tests files:

package.json

{
  "name": "my-project",
  "jest": {
    "testEnvironment": "./testEnvironment.js"
  }
}

testEnvironment.js

const express = require('express');
// for server node apps
// const NodeEnvironment = require('jest-environment-node');

// for browser js apps
const NodeEnvironment = require('jest-environment-jsdom');

class ExpressEnvironment extends NodeEnvironment {
    constructor(config, context) {
        super(config, context);
    }

    async setup() {
        await super.setup();
        let server;
        const app = express();
        await new Promise(function(resolve) {
            server = app.listen(0, "127.0.0.1", function() {
                let address = server.address();
                console.log(
                    ` Running server on '${JSON.stringify(address)}'...`);
                resolve();
            });
        });
        let address = server.address();
        this.global.server = server;
        this.global.address = `${address.address}:${address.port}`
        app.use(express.static('./testfiles'));
    }
    async teardown() {
        this.global.server.close();
        await super.teardown();
    }

    runScript(script) {
        return super.runScript(script);
    }
}

module.exports = ExpressEnvironment;

Then, you can access the this.global.server in your tests files to get the server port/address:

test.js

test('Show the server address as example', () => {
    // @ts-ignore: https://github.com/kulshekhar/ts-jest/issues/1533
    let address = global.address;
    console.log(`The server address is '${address}'...`)
});

Results:

$ npx jest
 PASS  src/reviewer.test.ts (5.391s)
  √ renders GitHub Repository Researcher site name (10ms)

Running server on '{"address":"127.0.0.1","family":"IPv4","port":50875}'.
  console.log src/reviewer.test.ts:25
    The server address is '127.0.0.1:50875'.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        6.811s
Ran all test suites.

Just remember the documentation warning:

Note: TestEnvironment is sandboxed. Each test suite/file will trigger setup/teardown in their own TestEnvironment.

https://github.com/heuels/jest/blob/master/docs/Configuration.md#available-in-jest-2200

Evandro Coan
  • 8,560
  • 11
  • 83
  • 144
1

You should create your server beforeEach() (and stop it in afterEach()) so that it runs for each test.

docs.

You should also pick an unused port so that tests can run in parallel.

SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964
  • How would I pick an unused port? Do I need have Express listen to a random port? The way to do that, if I'm not mistaken is to leave `app.listen()` empty, so then how would I add my callback? Also if I'm creating the server `beforeEach()` will I need to change my callback at all? – bkula Dec 04 '17 at 22:54
  • 2
    @bkula: Pass `0`. https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback – SLaks Dec 04 '17 at 22:56
  • Thanks, but I'm still confused about how my callback should look. If I pass `app.listen()` a callback that runs every test, doesn't that defeat the purpose of using `beforeEach()`? Do I need to loop through them somehow? Or will the code in `beforeEach()` still run before every test? – bkula Dec 04 '17 at 23:05
  • You should make `beforeEach()` async and pass its callback to `listen()`. – SLaks Dec 04 '17 at 23:13
  • I appreciate all the help and you pointing me in the right direction, but I'm still pretty in the dark about how this code is supposed to look. – bkula Dec 04 '17 at 23:24
1

Alternatively, you can use globalSetup and globalTeardown combined with testEnvironment

package.json

{
  "name": "my-project",
  "jest": {
    "testEnvironment": "./testEnvironment.js",
    "globalSetup": "./globalSetup.js",
    "globalTeardown": "./globalTeardown.js"
  }
}

globalTeardown.js

module.exports = async () => {
    global.server.close();
};

globalSetup.js

const express = require('express');

module.exports = async () => {
    let server;
    const app = express();

    await new Promise(function(resolve) {
        server = app.listen(0, "127.0.0.1", function() {
            let address = server.address();
            console.log(` Running express on '${JSON.stringify(address)}'...`);
            resolve();
        });
    });

    let address = server.address()
    global.server = server;
    process.env.SERVER_ADDRESS = `http://${address.address}:${address.port}`
    app.use(express.static('./testfiles'));
};

testEnvironment.js

const TestEnvironment = require('jest-environment-jsdom'); // for browser js apps
// const TestEnvironment = require('jest-environment-node'); // for server node apps

class ExpressEnvironment extends TestEnvironment {
    constructor(config, context) {
        let cloneconfig = Object.assign({}, config)
        cloneconfig.testURL = process.env.SERVER_ADDRESS;
        super(cloneconfig, context);
    }

    async setup() {
        this.global.jsdom = this.dom;
        await super.setup();
    }

    async teardown() {
        this.global.jsdom = null;
        await super.teardown();
    }

    runScript(script) {
        return super.runScript(script);
    }
}

module.exports = ExpressEnvironment;
Evandro Coan
  • 8,560
  • 11
  • 83
  • 144