Mocha/Chai/Chai-Http测试Express应用时单元测试随机无规律失败
Express + Mocha接口测试随机失败问题排查
问题背景
使用Mocha/Chai/Chai-Http工具链对Express应用开展接口测试时,大部分场景下测试用例可正常通过,但会随机出现部分用例失败的情况,无固定复现规律。
涉及的测试代码如下:
describe("projectTest :>> login", () => { // Create a user in order to have the access token to execute authenticated API before(function (done) { let person = new User({ email: "dev.test@test.com", password: "<hashed password>", firstName: "dev", lastName: "test", jobPosition: "Web Developer", }); person.save(); done(); }); /* * Test the /POST login route */ describe(" projectTest :>> /POST login ", () => { it("it should success if credential (person) is valid ", done => { chai .request(server) .post("/api/projectTest/account/login") .send({ email: "dev.test@test.com", password: "<password>", }) .end(function (error, response) { if (error) done(error); response.should.have.status(200); response.body.should.have.property("token"); done(); }); }); }); after(function () { User.deleteMany({}, function () {}); }); });
测试执行命令:
cross-env NODE_ENV=test PROJECT_NAME=projectTest mocha --timeout 20000 ./routes/projectTest/**/*.spec.js --exit
运行时抛出的错误信息:
Uncaught AssertionError: expected { Object (_events, _eventsCount, ...) } to have status code 200 but got 400
当前项目相关依赖版本:
chai 4.2.0mocha 10.0.0chai-http 4.3.0
问题根本诱因
- 核心问题是测试生命周期钩子的异步逻辑未做等待:
before钩子中person.save()是Mongoose的异步数据库写入操作,但代码在调用save后立刻执行了done(),没有等待数据真正落库。当数据库写入耗时比测试用例启动耗时长时,登录接口执行时测试用户数据还未写入完成,接口会返回400状态码(通常为用户不存在、凭证无效类错误),触发断言失败;当写入速度更快时测试就会正常通过,这就是随机失败的直接原因。 - 清理逻辑存在异步隐患:
after钩子中的User.deleteMany()操作同样没有等待执行完成就结束钩子,可能导致不同测试文件、用例之间出现测试数据污染,进一步提升随机失败的概率。 - 错误分支逻辑存在漏洞:接口请求回调中
if (error) done(error)执行后没有终止后续逻辑,当请求本身抛出错误时,后续的状态码断言依然会执行,会抛出未捕获的断言错误,干扰问题定位。
修复方案
- 补全
before钩子的异步等待逻辑,确认测试用户写入数据库后再进入测试用例:
before(function (done) { let person = new User({ email: "dev.test@test.com", password: "<hashed password>", firstName: "dev", lastName: "test", jobPosition: "Web Developer", }); // 等待save操作执行完成再调用done person.save(function(err) { if (err) return done(err); done(); }); });
如果项目支持async/await写法,也可以简化为:
before(async function () { let person = new User({ email: "dev.test@test.com", password: "<hashed password>", firstName: "dev", lastName: "test", jobPosition: "Web Developer", }); await person.save(); });
- 补全
after钩子的异步等待逻辑,确认测试数据清理完成后再结束钩子:
after(function (done) { User.deleteMany({}, function (err) { if (err) return done(err); done(); }); });
对应async写法:
after(async function () { await User.deleteMany({}); });
- 修正请求回调的错误分支,触发错误后直接返回,避免后续断言逻辑执行:
.end(function (error, response) { if (error) return done(error); response.should.have.status(200); response.body.should.have.property("token"); done(); });
完成以上修改后,测试用例的执行顺序会和异步逻辑严格对齐,不会再出现因数据未就绪导致的随机失败问题。
内容的提问来源于stack exchange,提问作者oussama Seffai
相关产品推荐
相关产品推荐

