如何为需身份认证的Express端点编写Jest单元测试?
为需认证的Express路由编写Jest单元测试
我有一个由4个文件组成的简易项目,想为bingo.js中需要身份认证的路由/编写Jest测试。项目结构如下:
index.js:Express应用入口文件routes/auth.js:用户认证的POST端点routes/bingo.js:需认证才能访问的端点middleware/authentication.js:检查会话用户的中间件
我不想通过index.js启动整个应用来写测试(端到端测试已经用Cypress覆盖了),尝试过调用认证步骤、模拟会话、在Supertest中设置会话等方法,但都没成功,希望得到解决方向。
项目文件代码
index.js
// index.js const express = require("express"); const cookieParser = require("cookie-parser"); const session = require("express-session"); const app = express(); const bingoRouter = require("./routes/bingo"); const authRouter = require("./routes/auth"); // middleware app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use( session({ cookie: { maxAge: 300000 }, store: new session.MemoryStore(), saveUninitialized: true, resave: "true", secret: "our_really_secret_key", }) ); app.use("/bingo", bingoRouter); app.use("/auth", authRouter); app.use((req, res) => { return res.status(404).json({ error: "Not Found", }); }); app.listen(port); module.exports = app;
auth.js
// routes/auth.js const express = require("express"); const router = express.Router(); const jwt = require("jsonwebtoken"); const secretKey = "our_really_secret_key"; // Authenticate user router.post("/login", async (req, res) => { const { username, password } = req.body; const user = { username: "username", password: "password", is_admin: true }; if (user && user.password === "password") { req.session.user = user; res.redirect("/bingo"); } else { res.render("auth", { error: "Invalid username or password" }); } });
bingo.js
// routes/bingo const { isAdmin } = require("../middleware/authentication"); const express = require("express"); const router = express.Router(); router.use(isAdmin); router.get("/", async function (req, res) { res.render("bingo", { user: req.session.user, }); }); module.exports = router;
authentication.js
// middleware/authentication.js exports.isAdmin = (req, res, next) => { if (req.session.user && req.session.user.is_admin) { // User is authenticated & admin, allow access to route next(); } else { // User is not authenticated or not admin, redirect to login page res.redirect("/auth"); } };
解决方案
方法1:拆分测试中间件与路由逻辑
不需要启动完整应用,直接针对isAdmin中间件和bingo.js的路由处理函数编写单元测试,手动模拟请求、响应和next函数。
创建routes/__tests__/bingo.test.js:
const { isAdmin } = require("../../middleware/authentication"); const bingoRouter = require("../bingo"); describe("bingo路由测试", () => { describe("isAdmin中间件", () => { test("允许已认证的管理员用户通过", () => { const req = { session: { user: { is_admin: true } } }; const res = { redirect: jest.fn() }; const next = jest.fn(); isAdmin(req, res, next); expect(next).toHaveBeenCalled(); expect(res.redirect).not.toHaveBeenCalled(); }); test("未认证用户被重定向到登录页", () => { const req = { session: {} }; const res = { redirect: jest.fn() }; const next = jest.fn(); isAdmin(req, res, next); expect(res.redirect).toHaveBeenCalledWith("/auth"); expect(next).not.toHaveBeenCalled(); }); test("非管理员用户被重定向到登录页", () => { const req = { session: { user: { is_admin: false } } }; const res = { redirect: jest.fn() }; const next = jest.fn(); isAdmin(req, res, next); expect(res.redirect).toHaveBeenCalledWith("/auth"); expect(next).not.toHaveBeenCalled(); }); }); describe("GET /bingo路由", () => { test("渲染bingo页面并传递用户信息", async () => { const mockUser = { username: "test", is_admin: true }; const req = { session: { user: mockUser } }; const res = { render: jest.fn() }; // 直接提取路由处理函数(中间件已单独测试) const getRoute = bingoRouter.stack.find(layer => layer.route?.path === "/" && layer.route?.methods.get ).handle; await getRoute(req, res); expect(res.render).toHaveBeenCalledWith("bingo", { user: mockUser }); }); }); });
方法2:用Supertest创建轻量测试应用
如果需要测试路由与中间件的集成,但不想依赖原项目的index.js,可以创建仅包含必要组件的测试专用应用:
const request = require("supertest"); const express = require("express"); const session = require("express-session"); const cookieParser = require("cookie-parser"); const bingoRouter = require("../routes/bingo"); describe("bingo路由集成测试", () => { let app; beforeEach(() => { app = express(); // 添加必要的会话与cookie中间件 app.use(cookieParser()); app.use( session({ secret: "test_secret", resave: false, saveUninitialized: true, store: new session.MemoryStore() }) ); app.use("/bingo", bingoRouter); }); test("已认证管理员可访问/bingo", async () => { const agent = request.agent(app); // 提前在会话中设置管理员用户 await agent.get("/setup-session").use((req, res, next) => { req.session.user = { is_admin: true }; req.session.save(next); }).send(); const response = await agent.get("/bingo"); expect(response.statusCode).not.toBe(302); // 无重定向 expect(response.text).toContain("bingo"); // 验证页面渲染内容 }); test("未认证用户被重定向到/auth", async () => { const response = await request(app).get("/bingo"); expect(response.statusCode).toBe(302); expect(response.headers.location).toBe("/auth"); }); });
核心思路
- 拆分测试关注点:先单独验证中间件的权限逻辑,再测试路由的渲染逻辑,避免耦合完整应用。
- 模拟核心对象:用Jest的
jest.fn()模拟req、res和next,精准验证函数调用行为。 - 轻量集成测试:如果需要验证路由与中间件的协作,搭建仅包含必要组件的测试应用,减少冗余依赖。
内容的提问来源于stack exchange,提问作者Dycey
相关产品推荐
相关产品推荐

