我是 Node 和 Express 的新手,我正在尝试对我的路由/控制器进行单元测试。我已将路线与控制器分开。我该如何测试我的路线?
config/express.js
var app = express();
// middleware, etc
var router = require('../app/router')(app);
应用程序/路由器/index.js
module.exports = function(app) {
app.use('/api/books', require('./routes/books'));
};
应用程序/路由器/routes/books.js
var controller = require('../../api/controllers/books');
var express = require('express');
var router = express.Router();
router.get('/', controller.index);
module.exports = router;
应用程序/api/controllers/books.js
// this is just an example controller
exports.index = function(req, res) {
return res.status(200).json('ok');
};
应用程序/测试/api/routes/books.test.js
var chai = require('chai');
var should = chai.should();
var sinon = require('sinon');
describe('BookRoute', function() {
});
如果您只想对路由的存在及其方法进行单元测试,您可以执行以下操作:
auth.router.js
import { Router } from 'express';
const router = Router();
router.post('/signup', signupValidation, signupUser);
router.post('/login', loginValidation, loginUser);
router.post('/reset', resetValidation, setPasswordReset);
export default router;
auth.router.spec.js
test('has routes', () => {
const routes = [
{ path: '/signup', method: 'post' },
{ path: '/login', method: 'post' },
{ path: '/reset', method: 'post' },
]
it.each(routes)('`$method` exists on $path', (route) => {
expect(router.stack.some((s) => Object.keys(s.route.methods).includes(route.method))).toBe(true)
expect(router.stack.some((s) => s.route.path === route.path)).toBe(true)
})
注意: 在示例测试名称中使用 $variables 仅适用于 Jest ^27.0.0
编辑: 感谢 Keith Yeh 建议将其放入
each()
声明中。我已相应更新了代码,旧代码如下:
auth.router.spec.js (OLD)
import router from '../auth.router';
test('has routes', () => {
const routes = [
{ path: '/signup', method: 'post' },
{ path: '/login', method: 'post' },
{ path: '/reset', method: 'post' }
]
routes.forEach((route) => {
const match = router.stack.find(
(s) => s.route.path === route.path && s.route.methods[route.method]
);
expect(match).toBeTruthy();
});
});
代码:
config/express.js
var app = express();
// middleware, etc
var router = require('../app/router')(app);
module.exports = app;
应用程序/测试/api/routes/books.test.js
var chai = require('chai');
var should = chai.should();
var sinon = require('sinon');
var request = require('supertest');
var app = require('config/express');
describe('BookRoute', function() {
request(app)
.get('/api/books')
.expect('Content-Type', /json/)
.expect('Content-Length', '4')
.expect(200, "ok")
.end(function(err, res){
if (err) throw err;
});
});
注意事项:
如果您的服务器在一组测试开始时需要初始状态(因为您正在执行会改变服务器状态的调用),则您需要编写一个函数来返回新配置的应用程序和每个组的开头测试。有一个 NPM 库:https://github.com/bahmutov/really-need,它允许您需要服务器的新实例化版本。
这很有趣,因为您已将控制器与路由器分开。我认为评论中提到的另一篇 StackOverflow 文章是测试控制器的好方法。对于单元测试要记住的是你到底在测试什么。您不需要编写测试来测试 Express 库,因为它可能有自己的单元测试。所以你只需要测试对库的调用。所以对于书籍路线,你只需要测试这一行代码:
router.get('/', controller.index);
我环顾四周,看看是否有一种明显的方法可以从 Express 库中获取路线列表,但我没有看到。您可能可以只查看库本身并检查其内部结构,看看您是否正确设置了路线。另一种选择是模拟它并检查您是否正确调用它。
这将变得非常复杂,因为您需要模拟 Javascript 的一些基本部分才能测试这一行代码。我是这样做的:
describe('BookRoute', function() {
it("should route / to books controller index", function() {
var controller = require('../../../api/controllers/books');
var orig_this = this;
var orig_load = require('module')._load;
var router = jasmine.createSpyObj('Router', ['get']);
var express = jasmine.createSpyObj('express', ['Router']);
express.Router.and.returnValues(router);
spyOn(require('module'), '_load').and.callFake(function() {
if (arguments[0] == 'express') {
return express;
} else {
return orig_load.apply(orig_this, arguments);
}
});
require("../../../router/routes/books");
expect(router.get).toHaveBeenCalledWith('/', controller.index);
});
});
这里发生的事情是我使用 Jasmine 的间谍函数来监视 module.js 中的 _load 函数,该函数处理所有的 require 调用。这样,当我们需要 books 路由器并且它调用 require('express') 时,我们可以返回我们使用 jasmine.createSpyObj 创建的 Express SpyObj。一旦我们用间谍对象替换了express,那么我们就可以让它返回我们的Router SpyObj,这将让我们监视router.get。然后我们可以检查以确保它是用“/”和controller.index调用的。
如果你想经常使用它,这可能会被制作成某种实用程序。
我通常通过使用更面向对象的方法来避免很多这样的事情,要么我在任何地方传递一些可以模拟测试的对象,要么你可以使用某种依赖注入,比如 Angular 使用。
这个答案是@jamie的改进:
import router from "../payment";
describe("has routes", () => {
function findRouteByName(routes: any, path: any) {
return routes.find(
(layer: any) => layer.route && layer.route.path === path
);
}
const routes = [
{ path: "/", method: "post" },
{ path: "/presale", method: "post" },
{ path: "/deny", method: "post" },
{ path: "/cancel", method: "post" },
{ path: "/refund", method: "post" },
{ path: "/issuer/info", method: "get" },
{ path: "/latest", method: "get" },
{ path: "/history", method: "get" },
{ path: "/issuer/:transactionId", method: "get" },
{ path: "/:paymentId", method: "post" },
{ path: "/wallet/cancel", method: "post" },
{ path: "/wallet/refund", method: "post" },
{ path: "/corporate/payment-amount", method: "get" },
{ path: "/corporate/history", method: "get" },
];
it.each(routes)("`$method` exists on $path", (route) => {
const expectedMethod = route.method;
const singleRouteLayer = findRouteByName(router.stack, route.path);
const receivedMethods = singleRouteLayer.route.methods;
// Method control
expect(Object.keys(receivedMethods).includes(expectedMethod)).toBe(true);
// Path control
expect(router.stack.some((s) => s.route.path === route.path)).toBe(true);
});
});
对杰米答案的改进:
验证匹配的方法和路径属于同一路由。
您可以直接匹配您的方法,而不是使用 Object.keys(...).includes() 搜索方法。
test.each(routes)("`$method` exists on $path", (route) => {
expect(
router.stack
.some(
(s) =>
s.route.path === route.path && s.route.methods[route.method]
)
).toBe(true);
});