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.parserOnIningClient (_http_client.js:522:21)
at HTTPParser.parserOnHeadersComplete (_http_mon.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?
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.parserOnIningClient (_http_client.js:522:21)
at HTTPParser.parserOnHeadersComplete (_http_mon.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?
Share Improve this question asked Dec 4, 2017 at 22:38 bkulabkula 5693 gold badges10 silver badges22 bronze badges 6- If it wasn't running in Node, Express itself would give errors. – SLaks Commented Dec 4, 2017 at 22:42
-
Side note: You aren't actually passing a callback to
listen()
. Passing one might solve your problem. – SLaks Commented Dec 4, 2017 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 makebeforeEach()
async and pass the callback tolisten()
. Islisten()
not supposed to be included inbeforeEach()
? – bkula Commented Dec 4, 2017 at 23:18 -
No; you're calling
tests()
and passing its return value (which isundefined
). Just like any other function call. – SLaks Commented Dec 5, 2017 at 0:59 -
You should call
listen()
inbeforeEach()
and pass it thedone
callback frombeforeEach()
. Read the documentation & learn how callbacks work. – SLaks Commented Dec 5, 2017 at 1:00
3 Answers
Reset to default 2To 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./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./heuels/jest/blob/master/docs/Configuration.md#available-in-jest-2200
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.
Alternatively, you can use globalSetup
and globalTeardown
bined 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;