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

Jest测试超时:article.controller.js的editArticle测试失败排查

Jest单元测试超时排查:editArticle测试用例超时问题

问题描述

在排查article.controller.js的单元测试时,Jest抛出超时错误:Exceeded timeout of 5000 ms for a test,错误定位到editArticle tests下的If user can edit an article测试用例,无法确定具体原因。

控制器代码(article.controller.js)

const {
  saveArticle,
  updateArticle,
  removeArticle,
  getArticle,
  getAllTags,
  getAllArticles,
  feedArticles,
  favoriteArticle,
  unFavoriteArticle,
} = require('../services/article.service');

exports.editArticle = async (req, res, next) => {
  const slugText = req.params.slug;
  if (slugText) {
    return updateArticle(slugText, req.user._id, req.body.article)
      .then((isUpdated) => {
        if (isUpdated) {
          return res.status(200).json({ article: isUpdated });
        }
      })
      .catch(next);
  }
};

单元测试代码

const articleServiceHelper = require("../../../services/article.service");

const mockResponse = () => {
  return {
    status: jest.fn().mockReturnThis(),
    json: jest.fn().mockReturnThis()
  };
};

const mockNext = jest.fn();

const mockUser = {
  _id: "6368ad4347dkhs"
};

const mockUpdatedArticle = {
  title: "Updated article title",
  description: "Updated description",
  body: "Updated body"
}

describe("Unit tests for article.controller.js file", () => {
  describe("editArticle tests", () => {
    test("If user can edit an article", async () => {
      //Arrange
      const mockReq = { params: { slug: 'test-article-slug' }, user: { _id: "6368ad4347dkhs" }, body: { article: mockUpdatedArticle } };
      const mockRes = mockResponse();
      const updateArticleSpy = jest.spyOn(articleServiceHelper, "updateArticle").mockResolvedValue(mockUpdatedArticle);
      const { editArticle } = require("../../../controllers/article.controller");
      //Act
      const updatedArticle = await editArticle(mockReq, mockRes, mockNext);
      //Assert
      expect(updatedArticle).toBe(mockUpdatedArticle);
      expect(updateArticleSpy).toHaveBeenCalledTimes(1);
      expect(updateArticleSpy).toHaveBeenCalledWith(mockReq.params.slug, mockUser._id, mockReq.body.article);
      expect(mockRes.status).toHaveBeenCalledWith(200);
      expect(mockRes.json).toHaveBeenCalledWith({
        article: mockUpdatedArticle
      });
      expect(mockNext).not.toHaveBeenCalled();
    });
  });
});

排查分析

超时的核心原因是控制器的editArticle函数存在未处理的Promise状态分支:

  1. 当updateArticle返回isUpdated时,控制器返回的是res.status(200).json(...)的结果——而res.json被mock成返回自身的链式调用对象,并非Promise,导致await editArticle(...)无法正确解析Promise状态,测试会一直等待直到超时。
  2. 测试中错误断言expect(updatedArticle).toBe(mockUpdatedArticle):editArticle的返回值是res.json()的mock对象,而非mockUpdatedArticle,这一错误逻辑也间接导致测试流程异常。
  3. 测试用例内部执行require("../../../controllers/article.controller"),可能引发模块缓存问题,干扰测试执行逻辑。

修复方案

1. 修正控制器代码(统一async/await语法)

将控制器改为纯async/await写法,确保所有分支都返回明确的Promise状态,避免混用.then():

exports.editArticle = async (req, res, next) => {
  try {
    const slugText = req.params.slug;
    if (!slugText) {
      return next(new Error("Slug is required"));
    }
    const isUpdated = await updateArticle(slugText, req.user._id, req.body.article);
    if (isUpdated) {
      return res.status(200).json({ article: isUpdated });
    }
    return res.status(404).json({ error: "Article not found or not authorized" });
  } catch (err) {
    next(err);
  }
};

2. 修正测试代码(匹配控制器实际行为)

  • 将模块require移到文件顶部,避免缓存问题
  • 移除错误的updatedArticle断言,聚焦验证服务调用和响应行为
const articleServiceHelper = require("../../../services/article.service");
const { editArticle } = require("../../../controllers/article.controller");

const mockResponse = () => {
  return {
    status: jest.fn().mockReturnThis(),
    json: jest.fn().mockReturnThis()
  };
};

const mockNext = jest.fn();

const mockUser = {
  _id: "6368ad4347dkhs"
};

const mockUpdatedArticle = {
  title: "Updated article title",
  description: "Updated description",
  body: "Updated body"
}

describe("Unit tests for article.controller.js file", () => {
  describe("editArticle tests", () => {
    test("If user can edit an article", async () => {
      //Arrange
      const mockReq = { 
        params: { slug: 'test-article-slug' }, 
        user: mockUser, 
        body: { article: mockUpdatedArticle } 
      };
      const mockRes = mockResponse();
      jest.spyOn(articleServiceHelper, "updateArticle").mockResolvedValue(mockUpdatedArticle);
      
      //Act
      await editArticle(mockReq, mockRes, mockNext);
      
      //Assert
      expect(articleServiceHelper.updateArticle).toHaveBeenCalledTimes(1);
      expect(articleServiceHelper.updateArticle).toHaveBeenCalledWith(
        mockReq.params.slug, 
        mockUser._id, 
        mockReq.body.article
      );
      expect(mockRes.status).toHaveBeenCalledWith(200);
      expect(mockRes.json).toHaveBeenCalledWith({
        article: mockUpdatedArticle
      });
      expect(mockNext).not.toHaveBeenCalled();
    });
  });
});

3. 临时应急方案(不修改控制器的情况下)

如果暂时无法修改控制器,可以调整mock让res.json返回Promise,确保测试能正确解析:

const mockResponse = () => {
  return {
    status: jest.fn().mockReturnThis(),
    json: jest.fn().mockResolvedValue(true)
  };
};

此方案仅为临时绕过问题,建议优先采用前两种彻底修正方案。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 05:55:10