You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为需身份认证的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");
  });
});

核心思路

  1. 拆分测试关注点:先单独验证中间件的权限逻辑,再测试路由的渲染逻辑,避免耦合完整应用。
  2. 模拟核心对象:用Jest的jest.fn()模拟req、res和next,精准验证函数调用行为。
  3. 轻量集成测试:如果需要验证路由与中间件的协作,搭建仅包含必要组件的测试应用,减少冗余依赖。

内容的提问来源于stack exchange,提问作者Dycey

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 22:53:16