I'm using Restify for my API and I'm trying to write tests for my endpoints.
At first, I had only a test for ping and it was okay, but now, after I added a new test, supertest is trying to ephemeral ports to the test server (80201).
I've searched a lot and tried some approaches that seem to work for most people but not for me. I'm probably messing something up, but I have no clue what it could be.
Check out my code:
server.js
require('dotenv').config();
const config = require('./config');
const routes = require('./src/routes');
const cors = require('restify-cors-middleware');
const http = require('http');
const https = require('https');
const restify = require('restify');
module.exports = function () {
http.globalAgent.keepAlive = true;
http.globalAgent.maxSockets = 256;
https.globalAgent.keepAlive = true;
https.globalAgent.maxSockets = 256;
const _cors = cors({
preflightMaxAge: 5,
origins: [new RegExp("^(https?:\/\/)?[-\w]+\.hackz\.co(\.\w+)?(:[\d]+)?$")],
allowHeaders: [
'authorization',
'x-requested-with',
'Content-MD5',
'Date',
'Accept-Version',
'Api-Version',
'Response-Time'
],
credentials: true
});
const server = restify.createServer({ name: config.apiName });
// Middlewares
server.pre(_cors.preflight);
server.use(_cors.actual);
server.use(restify.plugins.fullResponse());
server.use(restify.plugins.queryParser({ mapParams: true }));
server.use(restify.plugins.bodyParser({ mapParams: true }));
// Load Routes
routes.set(server);
server.on('error', function (req, res, route, error) {
if (error && (error.statusCode == null || error.statusCode !== 404)) {}
});
// Start Server
server.listen(config.apiPort, function () {
console.log(`${server.name} listening at ${server.url}.\nWe're in ${config.env} environment!`);
});
return server;
}();
tests/config/server.js
const server = require('../..');
const request = require('supertest');
function TestServer() {
return request(server);
}
module.exports = { TestServer };
tests/services/request.js
const { TestServer } = require("../config/server");
async function get(path, sessionkey = '', params = {}) {
const server = TestServer();
return await server
.get(path)
.query(params)
.set("authorization", sessionkey)
.set("content-type", "application/json")
;
}
async function post(path) {
const server = TestServer();
return await server
.post(path)
.set("content-type", "application/json")
;
}
module.exports = {
get,
post,
};
tests/config/setup.js
const server = require('../..');
afterAll(() => {
return server.close()
});
src/controllers/Ping.test.js
const { get } = require('../../tests/services/request');
describe('Ping Controller', () => {
describe('GET /ping', () => {
it('Should return 200', async () => {
const response = await get('/ping');
expect(response.status).toBe(200);
});
});
});
src/controllers/Session.test.js
const { post } = require('../../tests/services/request');
describe('Session Controller', () => {
const userId = 1;
describe('POST /:userId/create', () => {
it('Should create session successfully!', async () => {
const response = await post(`${userId}/create`);
expect(response.status).toBe(200);
expect(response.body.auth).toBe(true);
});
});
});
package.json (scripts and Jest config)
...
"scripts": {
"start": "node index.js",
"test": "jest --detectOpenHandles --forceExit --coverage",
"test:api": "npm run test -- --roots ./src/controllers"
},
...
"jest": {
"setupFilesAfterEnv": [
"jest-extended",
"<rootDir>/tests/config/setup.js"
],
...
}
This is the error output:
> user-session-api#1.0.0 test
> jest --detectOpenHandles --forceExit --coverage
FAIL src/controllers/Session.test.js
Session Controller
POST /:userId/create
✕ Should create session successfully! (39 ms)
● Session Controller › POST /:userId/create › Should create session successfully!
RangeError: Port should be >= 0 and < 65536. Received 80201.RangeError [ERR_SOCKET_BAD_PORT]: Port should be >= 0 and < 65536. Received 80201.
Things I've tried:
Passing the result of server.listen(...) (instead of the server instance) to supertest (as described here);
Using beforeEach for each test manually listening to a specific port;
This approach, which is similar to the first item.
HELP!
UPDATE:
Just realized that running npm run test "Ping.test.js" it succeeds and running npm run test "Session.test.js" (which is the new test) it fails. So there's probably something wrong with that single file.
I was also getting this error:
RangeError [ERR_SOCKET_BAD_PORT]: Port should be >= 0 and < 65536. Received 80201
Using supertest and Jest to test a NestJS application. I was also doing a
return request(app.getHttpServer())
.get(`route`)
Instead of
return request(app.getHttpServer())
.get(`/route`)
OH MY GOD!
I found the issue and, prepare yourself, the solution is ridiculous.
The request path in my test had a typo.
I was doing this:
const response = await post(`${userId}/create`); // with userId as 1
The path was missing an initial /, that's it haha.
THAT'S why supertest was appending a 1 to the server port and the RangeError was raised.
I'm hating myself right now.
Related
I have a simple test script for my API, the specific route is not implemented at the moment. But for some reason tests pass. Here is the script:
const request = require('supertest')
const api = require('../api-server')
// testing data:
const pmOne = {
name: "Some Name",
tel: "234 123"
}
const pmTwo = {
name: "Some Other Name",
tel: "256 789"
}
describe('Basic CRUD API', () =>{
it('GET /pm --> array of all projectmanagers', () =>{
request(api)
.post('/pm')
.send(pmTwo)
.expect(207)
})
it('GET /pm/id --> new projectmanager', () => {
request(api)
.get('/pm/' + pmOneId)
.expect(200)
.then((res) => {
expect(res.body.name).toBe(pmOne.name)
expect(res.body.tel).toBe(pmOne.tel)
})
})
And here is my app.js:
require('dotenv').config()
const express = require('express')
const mongoose = require('mongoose')
const app = express()
const pmRouter = require('./routes/pm.router')
// establish mongodb connection
var options = {
user: process.env.DATABASE_USER,
pass: process.env.DATABASE_PASSWD
}
mongoose.connect(process.env.DATABASE_URL, options)
const db = mongoose.connection
db.on('error', (error) => console.error(error))
app.use(express.json())
//app.use('/pm', pmRouter)
module.exports = app
The
//app.use('/pm', pmRouter)
Is commented out, so the route is not valid. That means all request should return 404 (which they do), so why am I getting passes on my tests:
> jest --forceExit --runInBand
PASS tests/projectmanager.test.js
Basic CRUD API
✓ GET /pm --> array of all projectmanagers (5 ms)
✓ GET /pm/id --> new projectmanager (3 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.571 s, estimated 1 s
Ran all test suites matching /tests\/projectmanager.test.js/i.
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?
Supertest request returns a promise and if you want to assert the returned value it has to be awaited or returned explicitly.
Either return it or use async/await syntax.
In your case simply add a return statement.
it('GET /pm', () => {
return request(api).post('/pm').send(pmTwo).expect(207)
});
I have multiple test files in my project. Each test file has almost similar beforeAll and afterAll functions which opens server and db connections and terminate them in the afterAll function.
I was getting error for server connection which said the port is already in use and I was able to handle that by not specifying a port number in test environment. However I'm unable to solve for db.
The lines below in .test.js file are common in all test files. The test itself is a dummy test. I need to have access to server in order to call the APIs there.
.test.js
const db = require('../service/pg');
const createServer = require('../service/server');
let server;
let sequelize;
beforeAll(async () => {
sequelize = await db.init();
server = await createServer();
await server.start();
});
afterAll(async () => {
await server.stop();
await sequelize.drop();
await sequelize.close();
});
describe('testing...', () => {
test('one + one = 2', async () => {
const options = {
method: 'POST',
url: '/math',
};
const res= await server.inject(options);
expect(1).toEqual(1);
});
server.js
if (process.env.NODE_ENV !== 'test') {
server = Hapi.server({
port: process.env.PORT || 5000,
routes: { cors: true },
});
}
db.js
const sequelize = require('./sequelize');
await sequelize.sync();
sequelize.js
const user_db = new Sequelize(
env.dbName,
env.user,
env.password,
{
host: env.host,
port: env.port,
dialect: 'postgres',
logging: false,
}
);
.... // add models
Ideal solution would be initializing server and DB only once for all test files. Some global setup stuff. But I'm unable to do that.
If I have like 2 test files, one test file works file but the other would give error like:
console.error
Connection to the database failed: UniqueConstraintError [SequelizeUniqueConstraintError]: Validation error
Instead of setting up your db in the beforeAll do it outside of the describe scope once and stop the servers afterwards.
const db = require('../service/pg');
const createServer = require('../service/server');
let server;
let sequelize;
(async () => {
sequelize = await db.init();
server = await createServer();
await server.start();
})();
describe('testing...', () => {
test('one + one = 2', async () => {
const options = {
method: 'POST',
url: '/math',
};
const res= await server.inject(options);
expect(1).toEqual(1);
});
});
(async () => {
await server.stop();
await sequelize.drop();
await sequelize.close();
})();
If I have like 2 test files, one test file works file but the other would give error like: console.error Connection to the database failed: UniqueConstraintError [SequelizeUniqueConstraintError]: Validation error
The reason for this is that jest runs tests in parallel and it could be that your second test starts before the first one has been finished and the afterAll has been called.
I am pretty new to Nodejs and i am learning Nodejs course on udemy, I am facing some trouble of listen EADDRINUSE: address already in use :::4000 while re-running integration tests multiple time. The first time its successful but afterward I am getting the above-mentioned error on the following line
const server = app.listen(port, () => {winston.info(`Listening on port ${port}`)});
I am pasting my index.js and two test files, if some can point me out it will be a great help for me.
Index.js
const Joi = require("#hapi/joi");
Joi.objectId = require("joi-objectid")(Joi);
const winston = require("winston");
const express = require("express");
const app = express();
require("./startup/logging")();
require("./startup/config")();
require("./startup/dbconnectivity")();
require("./startup/routes")(app);
const port = process.env.port || 4000;
const server = app.listen(port, () => {winston.info(`Listening on port ${port}`)});
// exporting server object to be used in integration tests.
module.exports = server;
**Integration test file for Genre**
const request = require("supertest");
let server;
const {Genere} = require("../../models/genere");
const {User} = require("../../models/user");
describe("/api/genere", () => {
beforeEach(() => {
console.log("Before each Genre");
server = require("../../index");
});
afterEach(async () => {
console.log("After each Genre");
await Genere.deleteMany({});
await server.close();
});
describe("/GET", () => {
it("should return list of generes", async() => {
await Genere.insertMany([
{name: "genre1"},
{name: "genre2"},
{name: "genre3"}
]);
const res = await request(server).get("/api/geners");
expect(res.status).toBe(200);
console.log("response body is : " + res.body);
expect(res.body.length).toBe(3);
expect(res.body.map(g => g.name)).toContain("genre1");
});
});
describe("/GET/:id", () => {
it("should return genre with id", async() => {
const genre = new Genere({name: "genre1"});
await genre.save();
const res = await request(server).get("/api/geners/"+ genre.id);
expect(res.status).toBe(200);
expect(res.body.name).toBe("genre1");
});
it("should return error with invalid id", async() => {
const genre = new Genere({name: "genre1"});
await genre.save();
const res = await request(server).get("/api/geners/1");
expect(res.status).toBe(404);
expect(res.text).toMatch(/Invalid/);
});
});
describe("/POST", () => {
it("should return 401 if not authorized", async() => {
const genere = new Genere({name: "genere1"});
const res = await request(server).post("/api/geners").send(genere);
expect(res.status).toBe(401);
});
it("should return 400 if the name is less than 4 chars", async() => {
const res = await createRequestWithGenre({name: "g1"});
expect(res.status).toBe(400);
});
it("should return 400 if the name is greater than 25 chars", async() => {
const genreName = Array(26).fill("a").join("");
const res = await createRequestWithGenre({name: genreName})
expect(res.status).toBe(400);
});
it("should return 201 with gener object if proper object is sent", async() => {
const res = await createRequestWithGenre({name: "genre1"})
expect(res.status).toBe(201);
expect(res.body).toHaveProperty("_id");
expect(res.body).toHaveProperty("name", "genre1");
const genre = await Genere.find({ name: "genre1"});
expect(genre).not.toBe(null);
});
async function createRequestWithGenre(genre) {
const token = new User().generateAuthToken();
return await request(server)
.post("/api/geners")
.set("x-auth-token", token)
.send(genre);
}
});
});
As soon as i add another file for integration test like the one below i started to get the error which is mentioned after this file code.
const {User} = require("../../models/user");
const {Genere} = require("../../models/genere");
const request = require("supertest");
let token;
describe("middleware", () => {
beforeEach(() => {
console.log("Before each Middleware");
token = new User().generateAuthToken();
server = require("../../index");
});
afterEach(async () => {
console.log("After each Middleware");
await Genere.deleteMany({});
await server.close();
});
const exec = async() => {
return await request(server)
.post("/api/geners")
.set("x-auth-token", token)
.send({name: "gener1"});
}
it("should return 400 if invalid JWT token is sent", async() => {
token = "invalid_token";
const res = await exec();
expect(res.status).toBe(400);
expect(res.text).toBe("Invalid auth token");
});
});
Console Error
middleware
✕ should return 400 if invalid JWT token is sent (510ms)
● middleware › should return 400 if invalid JWT token is sent
listen EADDRINUSE: address already in use :::4000
10 | require("./startup/routes")(app);
11 | const port = process.env.port || 4000;
> 12 | const server = app.listen(port, () => {winston.info(`Listening on port ${port}`)});
| ^
13 | // exporting server object to be used in integration tests.
14 | module.exports = server;
at Function.listen (node_modules/express/lib/application.js:618:24)
at Object.<anonymous> (index.js:12:20)
at Object.beforeEach (tests/integration/middleware.test.js:11:22)
If someone can help me why it fails on the multiple runs then it will be really helpful for me to understand why do we need to open and close server object every time.
Supertest is able to manage the setup/teardown of an express/koa app itself if you can import an instance of app without calling .listen() on it.
This involves structuring the code a little differently so app becomes a module, separate to the server .listen()
// app.js module
const app = require('express')()
require("./startup/logging")()
...
module.exports = app
Then the entrypoint for running the server imports the app then sets up the server with .listen()
// server.js entrypoint
const app = require('./app')
const port = process.env.port || 4000;
app.listen(port, () => {winston.info(`Listening on port ${port}`)});
When supertest uses the imported app, it will start its own server and listen on a random unused port without clashes.
// test
const request = require('supertest')
const app = require('./app')
request(app).get('/whatever')
The supertest "server" instance can be reused for multiple tests too
// reuse test
const supertest = require('supertest')
const app = require('./app')
describe('steps', () => {
const request = supertest(app)
it('should step1', async() => {
return request.get('/step1')
})
it('should step2', async() => {
return request.get('/step2')
})
})
One solution is to run jest with max workers specified to 1 which can be configured in your package.json in the following way:
"scripts": {
"test": "NODE_ENV=test jest --forceExit --detectOpenHandles --watchAll --maxWorkers=1"
},
If I understand your setup correctly, you have multiple intergration-test files which Jest will try to run in parallel (this is the default-mode). The error you're getting makes sense, since for each suite a new server instance is created before each test, but the server might already have been started while executing a different suite.
As described in the offical documentation instead of beforeEach it would make sense to use globalSetup where you would init your server once before running all test suites and stop the server afterwards:
// setup.js
module.exports = async () => {
// ...
// Set reference to your node server in order to close it during teardown.
global.__MY_NODE_SERVER__ = require("../../index");
};
// teardown.js
module.exports = async function() {
await global.__MY_NODE_SERVER__.stop();
};
// in your jest-config you'd set the path to these files:
module.exports = {
globalSetup: "<rootDir>/setup.js",
globalTeardown: "<rootDir>/teardown.js",
};
Alternatively you could run your tests with the --runInBand option and beforeAll instead of beforeEach in order to make sure that only one server is created before each test, but I'd recommend the first option.
I created this middleware which executing only once when any route in the website gets the first hit from a visitor:
// pg-promise
const db = require('./db/pgp').db;
const pgp = require('./db/pgp').pgp;
app.use(async (ctx, next) => {
try {
ctx.db = db;
ctx.pgp = pgp;
} catch (err) {
debugErr(`PGP ERROR: ${err.message}` || err);
}
await next();
});
// One-Time middleware
// https://github.com/expressjs/express/issues/2457
const oneTime = (fn) => {
try {
let done = false;
const res = (ctx, next) => {
if (done === false) {
fn(ctx, next);
done = true;
}
next();
};
return res;
} catch (err) {
debugErr(`oneTime ERROR: ${err.message}` || err);
}
};
const oneTimeQuery = async (ctx) => {
const result = await ctx.db.proc('version', [], a => a.version);
debugLog(result);
};
app.use(oneTime(oneTimeQuery));
This code executing on the first-time only when a user visiting the website, resulting:
app:log Listening on port 3000 +13ms
app:req GET / 200 - 24ms +2s
23:07:15 connect(postgres#postgres)
23:07:15 SELECT * FROM version()
23:07:15 disconnect(postgres#postgres)
app:log PostgreSQL 9.6.2, compiled by Visual C++ build 1800, 64-bit +125ms
My problem is that I want to execute it at the server start, when there's no any visit on the site.
The future purpose of this code will be to check the existence of tables in the database.
Solution:
Placing this in ./bin/www before the const server = http.createServer(app.callback()); declaration helped:
const query = async () => {
const db = require('../db/pgp').db;
const pgp = require('../db/pgp').pgp;
const result = await db.proc('version', [], a => a.version);
debugLog(`www: ${result}`);
pgp.end(); // for immediate app exit, closing the connection pool (synchronous)
};
query();
You could start your application using a js script that requires your app and uses node's native http module to fire up the server. Exactly like in koa-generator (click).
This is in your app.js file:
const app = require('koa')();
...
module.exports = app;
And then this is in your script to fire up the server:
const app = require('./app');
const http = require('http');
[this is the place where you should run your code before server starts]
const server = http.createServer(app.callback());
server.listen(port);
Afterwards you start your application with:
node [script_name].js
Of course keep in mind the async nature of node when doing it this way. What I mean by that - run the 'listen' method on 'server' variable in callback/promise.
I've noticed that I'm writing http://localhost everytime I want to run a node test with superagent.
import superagent from 'superagent';
const request = superagent.agent();
request
.get('http://localhost/whatever')
.end((err, res) => { ... });
Is there any way of avoiding the localhost part?
As far as I've gone is to avoid the request being hardcoded to the host:
const baseUrl = 'http://localhost:3030';
request
.get(`${baseUrl}/whatever`)
But I still have to carry the baseUrl with the agent everytime.
While not so recently updated a package as superagent-absolute, superagent-prefix is officially recommended, and has the highest adoption as of 2020.
It is such a simple package that I would not be concerned with the lack of updates.
Example usage:
import superagent from "superagent"
import prefix from "superagent-prefix"
const baseURL = "https://foo.bar/api/"
const client = superagent.use(prefix(baseURL))
TL;DR: superagent-absolute does exactly that.
Detailed:
You can create one abstraction layer on top of superagent.
function superagentAbsolute(agent) {
return baseUrl => ({
get: url => url.startsWith('/') ? agent.get(baseUrl + url) : agent.get(url),
});
}
⬑ That would override the agent.get when called with a starting /
global.request = superagentAbsolute(agent)('http://localhost:3030');
Now you would need to do the same for: DELETE, HEAD, PATCH, POST and PUT.
https://github.com/zurfyx/superagent-absolute/blob/master/index.js
const OVERRIDE = 'delete,get,head,patch,post,put'.split(',');
const superagentAbsolute = agent => baseUrl => (
new Proxy(agent, {
get(target, propertyName) {
return (...params) => {
if (OVERRIDE.indexOf(propertyName) !== -1
&& params.length > 0
&& typeof params[0] === 'string'
&& params[0].startsWith('/')) {
const absoluteUrl = baseUrl + params[0];
return target[propertyName](absoluteUrl, ...params.slice(1));
}
return target[propertyName](...params);
};
},
})
);
Or you can simply use superagent-absolute.
const superagent = require('superagent');
const superagentAbsolute = require('superagent-absolute');
const agent = superagent.agent();
const request = superagentAbsolute(agent)('http://localhost:3030');
it('should should display "It works!"', (done) => {
request
.get('/') // Requests "http://localhost:3030/".
.end((err, res) => {
expect(res.status).to.equal(200);
expect(res.body).to.eql({ msg: 'It works!' });
done();
});
});