如何使用 Express 进行单元测试路由?

我正在学习 Node.js 的过程中,并且一直在使用 特快。非常喜欢这个框架; 但是,我在为一个路由编写单元/集成测试时遇到了麻烦。

能够对简单的模块进行单元测试是很容易的,并且已经使用 摩卡进行了单元测试; 然而,我使用 Express 进行的单元测试失败了,因为我传入的响应对象没有保留这些值。

路线-测试中的功能(路线/index.js) :

exports.index = function(req, res){
res.render('index', { title: 'Express' })
};

单元测试模组:

var should = require("should")
, routes = require("../routes");


var request = {};
var response = {
viewName: ""
, data : {}
, render: function(view, viewData) {
viewName = view;
data = viewData;
}
};


describe("Routing", function(){
describe("Default Route", function(){
it("should provide the a title and the index view name", function(){
routes.index(request, response);
response.viewName.should.equal("index");
});


});
});

当我运行这个命令时,它会因为“错误: 检测到全局泄漏: viewName,data”而失败。

  1. 我哪里做错了,才能让它运转起来?

  2. 有没有更好的方法在这个级别对代码进行单元测试?

更新 1. 修正了代码片段,因为我最初忘记了“ it ()”。

94165 次浏览

Change your response object:

var response = {
viewName: ""
, data : {}
, render: function(view, viewData) {
this.viewName = view;
this.data = viewData;
}
};

And it will work.

The easiest way to test HTTP with express is to steal TJ's http helper

I personally use his helper

it("should do something", function (done) {
request(app())
.get('/session/new')
.expect('GET', done)
})

If you want to specifically test your routes object, then pass in correct mocks

describe("Default Route", function(){
it("should provide the a title and the index view name", function(done){
routes.index({}, {
render: function (viewName) {
viewName.should.equal("index")
done()
}
})
})
})

As others have recommended in comments, it looks like the canonical way to test Express controllers is through supertest.

An example test might look like this:

describe('GET /users', function(){
it('respond with json', function(done){
request(app)
.get('/users')
.set('Accept', 'application/json')
.expect(200)
.end(function(err, res){
if (err) return done(err);
done()
});
})
});

Upside: you can test your entire stack in one go.

Downside: it feels and acts a bit like integration testing.

I've come to the conclusion that the only way to really unit test express applications is to maintain a lot of separation between the request handlers and your core logic.

Thus, your application logic should be in separate modules that can be required and unit tested, and have minimal dependence on the Express Request and Response classes as such.

Then in the request handlers you need to call appropriate methods of your core logic classes.

I'll put an example up once I've finished restructuring my current app!

I guess something like this? (Feel free to fork the gist or comment, I'm still exploring this).

Edit

Here's a tiny example, inline. See the gist for a more detailed example.

/// usercontroller.js
var UserController = {
_database: null,
setDatabase: function(db) { this._database = db; },


findUserByEmail: function(email, callback) {
this._database.collection('usercollection').findOne({ email: email }, callback);
}
};


module.exports = UserController;


/// routes.js


/* GET user by email */
router.get('/:email', function(req, res) {
var UserController = require('./usercontroller');
UserController.setDB(databaseHandleFromSomewhere);
UserController.findUserByEmail(req.params.email, function(err, result) {
if (err) throw err;
res.json(result);
});
});

if unit testing with express 4 note this example from gjohnson :

var express = require('express');
var request = require('supertest');
var app = express();
var router = express.Router();
router.get('/user', function(req, res){
res.send(200, { name: 'tobi' });
});
app.use(router);
request(app)
.get('/user')
.expect('Content-Type', /json/)
.expect('Content-Length', '15')
.expect(200)
.end(function(err, res){
if (err) throw err;
});

I was wondering this as well, but specifically for unit tests and not integration tests. This is what I'm doing right now,

test('/api base path', function onTest(t) {
t.plan(1);


var path = routerObj.path;


t.equals(path, '/api');
});




test('Subrouters loaded', function onTest(t) {
t.plan(1);


var router = routerObj.router;


t.equals(router.stack.length, 5);
});

Where the routerObj is just {router: expressRouter, path: '/api'}. I then load in subrouters with var loginRouterInfo = require('./login')(express.Router({mergeParams: true})); and then the express app calls an init-function taking in the express router as a parameter. The initRouter then calls router.use(loginRouterInfo.path, loginRouterInfo.router); to mount the subrouter.

The subrouter can be tested with:

var test = require('tape');
var routerInit = require('../login');
var express = require('express');
var routerObj = routerInit(express.Router());


test('/login base path', function onTest(t) {
t.plan(1);


var path = routerObj.path;


t.equals(path, '/login');
});




test('GET /', function onTest(t) {
t.plan(2);


var route = routerObj.router.stack[0].route;


var routeGetMethod = route.methods.get;
t.equals(routeGetMethod, true);


var routePath = route.path;
t.equals(routePath, '/');
});

To achieve unit testing instead of integration testing, I mocked the response object of the request handler.

/* app.js */
import endpointHandler from './endpointHandler';
// ...
app.post('/endpoint', endpointHandler);
// ...


/* endpointHandler.js */
const endpointHandler = (req, res) => {
try {
const { username, location } = req.body;


if (!(username && location)) {
throw ({ status: 400, message: 'Missing parameters' });
}


res.status(200).json({
location,
user,
message: 'Thanks for sharing your location with me.',
});
} catch (error) {
console.error(error);
res.status(error.status).send(error.message);
}
};


export default endpointHandler;


/* response.mock.js */
import { EventEmitter } from 'events';


class Response extends EventEmitter {
private resStatus;


json(response, status) {
this.send(response, status);
}


send(response, status) {
this.emit('response', {
response,
status: this.resStatus || status,
});
}


status(status) {
this.resStatus = status;
return this;
}
}


export default Response;


/* endpointHandler.test.js */
import Response from './response.mock';
import endpointHandler from './endpointHander';


describe('endpoint handler test suite', () => {
it('should fail on empty body', (done) => {
const res = new Response();


res.on('response', (response) => {
expect(response.status).toBe(400);
done();
});


endpointHandler({ body: {} }, res);
});
});

Then, to achieve integration testing, you can mock your endpointHandler and call the endpoint with supertest.

In my case the only I wanted to test is if the right handler has been called. I wanted to use supertest to laverage the simplicity of making the requests to the routing middleware. I am using Typescript a and this is the solution that worked for me

// ProductController.ts


import { Request, Response } from "express";


class ProductController {
getAll(req: Request, res: Response): void {
console.log("this has not been implemented yet");
}
}
export default ProductController

The routes

// routes.ts
import ProductController  from "./ProductController"


const app = express();
const productController = new ProductController();
app.get("/product", productController.getAll);

The tests

// routes.test.ts


import request from "supertest";
import { Request, Response } from "express";


const mockGetAll = jest
.fn()
.mockImplementation((req: Request, res: Response) => {
res.send({ value: "Hello visitor from the future" });
});


jest.doMock("./ProductController", () => {
return jest.fn().mockImplementation(() => {
return {
getAll: mockGetAll,


};
});
});


import app from "./routes";


describe("Routes", () => {
beforeEach(() => {
mockGetAll.mockImplementation((req: Request, res: Response) => {
res.send({ value: "You can also change the implementation" });
});
});


it("GET /product integration test", async () => {
const result = await request(app).get("/product");


expect(mockGetAll).toHaveBeenCalledTimes(1);


});






it("GET an undefined route should return status 404", async () => {
const response = await request(app).get("/random");
expect(response.status).toBe(404);
});
});




I had some issues to get the mocking to work. but using jest.doMock and the specific order you see in the example makes it work.

If you want to avoid supertest, you can simply mock the request and Response and test it just like any other async function.

  let handlerStatus = 0;
let handlerResponse: any = {}; // can replace any with the strong type


const req: Request = {
//  inject here the request details
headers: { authorization: 'XXXXX' },
} as Request;


const res = {
json(body: any) { // can replace any with the strong type
handlerResponse = body;
},
status(status: number) {
handlerStatus = status;
return this;
},
} ;


await callYourHanlderFunction(req, res as Response);


expect(handlerStatus).toBe(200);
expect(handlerResponse).toEqual(correctResponse);