I am trying to test some API endpoints without actually hitting the database (happens when using supertest). So I am trying to stub/fake the routes and just force a return. The other issue is if I wanted to mock the internal methods that are in the routes, I am having problems doing that as well.
The API is structured like this:
controller.js
export const route = "/named-route";
export const controller = new Router();
controller.get("/:id", async (req, res, next) => {
try {
const post = await getPost(req);
res.status(200).json(post);
} catch (err) {
next(err);
}
});
router.js
import {
controller as myController,
route as myRoute
} from "./controller.js";
router.use(myRoute, myController);
const router = Router();
export default router;
app.js
import router from "./router.js";
const app = express();
app.use(router);
export default app;
My testing attempts
import { expect } from "chai";
import sinon from "sinon";
import request from "supertest";
import app from "./app.js";
import { controller } from "./controller.js";
describe("my controller", function () {
describe("GET", function () {
it("tries to get a post", async function () {
// attempt 1 -- times out
const controllerStub = sinon.stub(controller, "get").resolves({..json});
await request(controllerStub)
.get("/named-route")
.then(async (res) => {
sinon.assert.match(res.body.statusCode, 200);
});
// attempt 2 -- times out
const controllerStub = sinon.stub(controller, "get").callsFake(
async () => new Promise((resolve) => {
resolve({statusCode: 200})
});
);
// same test call as above
// works but hits actual DB, problematic for POST type requests
const response = await request(app)
.get("/named-route");
expect(response.statusCode).to.equal(200);
});
});
});
Related
I am testing a nodejs express API with a PG db wrapped with Prisma ORM
I configured a testing singleton instance as described by Prisma docs
Since the API is implemented in CommonJS and not TS, I had to make some changes as described in this beautiful page.
Here is a synthesis of what I did, will try to make it short, so it's easier to read
orgs.js (A GET route served by the mock server later on ...)
const Router = require('express-promise-router')
const router = new Router()
const PrismaPool = require('../db/PrismaPool');
module.exports = router
router.get('/assessments', async (req, res) => {
try{
const prisma = PrismaPool.getInstance();
const data = await prisma.org.findUnique({
select:{
assessments:true,
},
where: {
id: res.locals.orgId,
},
})
res.send(data)
}
catch(err){
handleError(err, "[GET]/orgs/assessments", 400, req, res)
}
})
PrismaPool.js (A wrapper to access the unique prisma client instance)
const prisma = require('./PrismaClientInstance').default
class PrismaPool {
constructor() {
throw new Error('Use PrismaPool.getInstance()');
}
static getInstance() {
return prisma
}
}
module.exports = PrismaPool;
PrismaClientInstance.js The unique instance of PrismaClient class. This is the tricky part stitching between the CommonJS world and the TS world.
'use strict';
exports.__esModule = true;
const { PrismaClient } = require('#prisma/client')
const prisma = new PrismaClient()
exports['default'] = prisma;
All this configuration works GREAT at runtime, now, when wrapping it with JEST in unit tests, things go south quickly ...
mock_server.js (a simplified server to expose the orgs API above)
const http = require('http');
const express = require('express');
var orgsRouter = require('../orgs');
const app = express();
app.use('/orgs', orgsRouter);
const port = 3011
app.set('port', port);
const server = http.createServer(app);
function onError(error) {
// herror handling
}
function onListening() {
// some debug messages
}
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
module.exports = server
PrismaSingletonForTesting.ts (A jest deep mock of the PrismaClient instance)
import { PrismaClient } from '#prisma/client'
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'
import prisma from './PrismaClientInstance'
jest.mock('./PrismaClientInstance', () => ({
__esModule: true,
default: mockDeep<PrismaClient>()
}))
beforeEach(() => {
mockReset(prismaMock)
})
export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>
orgs.test.js (The tests of the orgs API)
const Request = require("request")
const { prismaMock } = require('../../db/PrismaSingletonForTesting')
const TEST_ORGID = 1
describe('egrest server', () => {
let server
beforeAll(() => {
server = require('./test_server')
})
afterAll(() => {
server.close()
})
describe('assessments', () => {
let data = {}
beforeAll(() => {
const testorg = {
id: TEST_ORGID,
name: 'jestest',
admin:33,
avail_tests: 1234
}
prismaMock.org.findUnique.mockResolvedValue(testorg)
})
it(`read remaining assessments for org ${TEST_ORGID}`, (done) => {
Request.get("http://localhost:3011/orgs/assessments", (error, response, body) => {
data.status = response.statusCode
data.body = body
data.error = error
console.dir(body)
done()
})
})
})
})
I also configured .jest.config with the required line setupFilesAfterEnv: ['./db/PrismaSingletonForTesting.ts']
When I run this test, I get data=undefined in orgs.js, even-though I mocked prisma.org.findUnique by doing prismaMock.org.findUnique.mockResolvedValue(testorg) as described by prisma docs.
Any help would be appreciated.
I will just say first of all that I am aware of almost all the questions asked on this site under this title.
The solutions there were pretty obvious and already done by me (with no success) or only helped for those specific cases and didn’t really work in my case unfortunately.
Now, for the problem:
I'm trying to create a route that will handle a get request and a post request which are sent to the route 'ellipses'.
These requests should receive and send data from and to an SQL database.
The problem is that for some reason the router is not ready to get these functions and gives me the error in the title:
Router.use () requires middleware function but got an undefined
Here is my code:
This code is from the file dat.js. its porpose is just to access the SQL database.
import { Sequelize } from "sequelize";
export const sequelize = new Sequelize('TheDataBaseName', 'TheUser', 'ThePassword', {
host: 'localhost',
dialect: 'mssql'
});
This code is from the file: controller.js. its porpose is to manage the requests and load the data.
import { sequelize } from "../dat";
export const sendEllipses = async (req, res, next) => {
try {
const ellipses = await getEllipsesFromJson();
return res.send(ellipses);
} catch (e) {
console.log(e);
}
};
export const addNewEllipse = async (req, res, next) => {
const { body: obj } = req;
let newEllipse;
try {
if (Object.keys(obj) !== null) {
logger.info(obj);
newEllipse = await sequelize.query(
`INSERT INTO [armageddon].[dbo].[ellipses] (${Object.keys(
obj
).toString()})
values (${Object.values(obj).toString()})`
);
} else {
console.log("the values are null or are empty");
}
return res.send(newEllipse);
} catch (error) {
console.log(error);
}
};
This code is on the file: routers.js.
its porpose is to define the route
import Router from "express";
import { sendEllipses } from "../ellipses.controller";
import { addNewEllipse } from "../ellipses.controller";
const router = Router();
export default router.route("/ellipses").get(sendEllipses).post(addNewEllipse);
This code is from the file: app.js. This is where everything actually happens.
import { router } from "../routers";
import express from "express";
app.use('/api', router);
app.listen(5000, () => {
console.log("server is runing on port 5000")
});
You need to export the router
const router = Router();
router.route("/ellipses").get(sendEllipses).post(addNewEllipse)
export default router
Now import the router:
import routes from "../router.js";
app.use('/api', routes);
Its also mentioned in the docs: https://expressjs.com/de/guide/routing.html#express-router
I have built my portfolio webpage with next.js now I need to test it. to test the express server I use supertest. But the problem is I need to refactor express to use it. Because supertest need to access to app() before listening.
I started the way how I used to implement in node.js app. Put the express code in app.js and call it in index.js.
const express = require("express");
const server = express();
const authService = require("./services/auth");
const bodyParser = require("body-parser");
//put all the middlewares here
module.exports = server;
and then in index.js
const server = require("express")();
// const { parse } = require("url");
const next = require("next");
const routes = require("../routes");
const path = require("path");
require("./mongodb");
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
// const handle = app.getRequestHandler(); //this is built in next route handler
const handle = routes.getRequestHandler(app);
app
.prepare()
.then(() => {
const server = require("./app");
//I required this outside too but it did not solve the issue
server.listen(3000, (err) => {
if (err) throw err;
console.log("> Ready on http://localhost:3000");
});
})
.catch((ex) => {
console.error(ex.stack);
process.exit(1);
});
with this set up, express is listening, I am able connect to mongodb, during the start up there is no issue.
When i request to localhost:3000, there is no response from localhost, it is spinning till timeout
Create a test client:
// test-client.ts
import { createServer, RequestListener } from "http";
import { NextApiHandler } from "next";
import { apiResolver } from "next/dist/next-server/server/api-utils";
import request from "supertest";
export const testClient = (handler: NextApiHandler) => {
const listener: RequestListener = (req, res) => {
return apiResolver(
req,
res,
undefined,
handler,
{
previewModeEncryptionKey: "",
previewModeId: "",
previewModeSigningKey: "",
},
false
);
};
return request(createServer(listener));
};
Test your APIs with:
// user.test.ts
import viewerApiHandler from "../api/user";
import { testClient } from "../utils/test-client";
const request = testClient(viewerApiHandler);
describe("/user", () => {
it("should return current user", async () => {
const res = await request.get("/user");
expect(res.status).toBe(200);
expect(res.body).toStrictEqual({ name: "Jane Doe" });
});
});
For those who want to add query parameters, here's the answer:
import { createServer, RequestListener } from 'http'
import { NextApiHandler } from 'next'
import { apiResolver } from 'next/dist/server/api-utils/node'
import request from 'supertest'
export const handlerRequester = (handler: NextApiHandler) => {
const listener: RequestListener = (req, res) => {
let query = {}
let queryUrl = req.url.split('?')[1]
if (queryUrl) {
queryUrl
.split('&')
.map((p) => [p.split('=')[0], p.split('=')[1]])
.forEach((k) => {
query[k[0]] = k[1]
})
}
return apiResolver(
req,
res,
query,
handler,
{
previewModeEncryptionKey: '',
previewModeId: '',
previewModeSigningKey: '',
},
false
)
}
const server = createServer(listener)
return [request(server), server]
}
I've just released a new npm package which handle this case here:
https://www.npmjs.com/package/nextjs-http-supertest
Feel free to test it and give me feedback !
I'm attempting to use Jest to unit test an express API I've been working on however the database has to be ready before it runs the test. This does not seem to be happening however. I have a server.ts file which contains:
import App from './app';
import UsersController from './controllers/users.controller';
const app = new App();
app.initialize(
[
new UsersController(),
]
);
if (process.env.NODE_ENV !== 'test') {
app.listen();
}
export default app;
app.initialize is an aysnc function which configures the database and routes my controllers.
In my unit test I then have the following
import server from "../server";
import supertest from 'supertest';
const request = supertest(server.app);
it('should allow users to register', async () => {
// Arrange
const user = {
firstName: 'John',
lastName: 'Smith',
age: 42
};
return request.post('/api/users')
.send(user)
.set('Accept', 'application/json')
.then(response => {
expect(response).toEqual(user.firstName)
expect(response.body.lastName).toEqual(user.lastName)
expect(response.body.id).toBeGreaterThan(0)
});
});
This however falls over with a 404 error, however if I remove the NODE_ENV check on "test" in the server file I can see that app.listen() get's called well after my test so I believe it's safe to assume that the tests are running before that file has finished.
For completeness here is my App class:
import "reflect-metadata";
import express from 'express';
import * as bodyParser from 'body-parser';
import {createConnection} from "typeorm";
import IController from './controllers/baseController.interface';
class App {
public app = express();
public port: number = 8080;
public ready: boolean = false;
public async initialize(controllers : [IController]) {
await createConnection().then(async connection => {
connection.synchronize();
this.initializeMiddlewares();
this.initializeControllers(controllers);
});
}
private initializeMiddlewares() {
this.app.use(bodyParser.json());
}
private initializeControllers(controllers : [IController]) {
controllers.forEach((controller) => {
this.app.use('/api/', controller.router);
});
}
public listen() {
this.app.listen(this.port, () => {
console.log(`App listening on the port ${this.port}`);
});
}
}
export default App;
You can try wrapping your App instance creation code inside a function. You can then wait for it inside your tests. In your server.ts do the following:
import App from './app';
import UsersController from './controllers/users.controller';
const getApp = async () => {
const app = new App();
await app.initialize(
[
new UsersController(),
]
);
}
getApp().then( appInstance => {
if (process.env.NODE_ENV !== 'test') {
appInstance.listen();
}})
export default getApp;
In your test file just call the function to get your app instance:
import getApp from "../server";
import supertest from 'supertest';
it('should allow users to register', async () => {
const app = await getApp()
const request = supertest(app);
// Arrange
const user = {
firstName: 'John',
lastName: 'Smith',
age: 42
};
return request.post('/api/users')
.send(user)
.set('Accept', 'application/json')
.then(response => {
expect(response).toEqual(user.firstName)
expect(response.body.lastName).toEqual(user.lastName)
expect(response.body.id).toBeGreaterThan(0)
});
});
I have a 404.jade file that I want to render whenever there is an invalid GET request.
Here is my current code:
app.js
import Koa from 'koa'
import views from 'koa-views'
import serve from 'koa-static'
import rootRoutes from './routes/index'
import userRoutes from './routes/user'
const app = new Koa()
app.use(views(`${__dirname}/views`, { extension: 'jade' }))
app.use(serve(`${__dirname}/public`))
app.use(rootRoutes.routes())
app.use(userRoutes.routes())
app.listen(3000, () => {
console.log('Server running at http://localhost:3000')
})
export default app
routes/index.js
import Router from 'koa-router'
const router = new Router()
router.get('/', async ctx => {
await ctx.render('index')
})
router.get('/about', async ctx => {
await ctx.render('about')
})
export default router
routes/user.js
import Router from 'koa-router'
const router = new Router({ prefix: '/user' })
router.get('/:name', async ctx => {
const user = ctx.params.name
await ctx.render('user', { user })
})
export default router
How can I handle any type of invalid GET request and somehow use await ctx.render('404') whenever it happens?
You can add a custom middleware in your app.js file.
import Koa from 'koa'
import views from 'koa-views'
import serve from 'koa-static'
import rootRoutes from './routes/index'
import userRoutes from './routes/user'
const app = new Koa()
app.use(async(ctx, next) => {
try {
await next()
const status = ctx.status || 404
if (status === 404) {
ctx.throw(404)
}
} catch (err) {
ctx.status = err.status || 500
if (ctx.status === 404) {
//Your 404.jade
await ctx.render('404')
} else {
//other_error jade
await ctx.render('other_error')
}
}
})
app.use(views(`${__dirname}/views`, { extension: 'jade' }))
app.use(serve(`${__dirname}/public`))
app.use(rootRoutes.routes())
app.use(userRoutes.routes())
app.listen(3000, () => {
console.log('Server running at http://localhost:3000')
})
export default app
The default value of ctx.response.status is 404
application.js line 125 :
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
const handleRequest = (req, res) => {
res.statusCode = 404; // defaul
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fn(ctx).then(handleResponse).catch(onerror);
};
return handleRequest;
}
and if you call :
this.render('index',{});
this.send();
this.body='';
the status code will change automatically.
So we can just use this :
app.use(async (ctx, next) => {
if(parseInt(ctx.status) === 404){
ctx.status = 404
ctx.body = {msg:'emmmmmmm, seems 404'};
}
})
Warning here, if you are using koa-router, make sure the function above is called with app.use( app = new Koa() ), not router.use