升级Chai 5.x并转为ESM模块后测试报错,无有效错误信息
问题描述
我有一个2018-2019年用require和module.exports编写的Node.js服务器,近期升级到Chai 5.1版本后,将项目转为ESM模块(使用import/export default),并在package.json中添加了:
"type": "module",
以及Mocha配置:
"mocha": { "node-option": [ "experimental-specifier-resolution=node" ] }
当前环境:Node.js 20.17.0、Mocha 10.7.3、Chai 5.1.1。
现在所有测试无法运行,报错无有效行号信息:
Exception during run: SyntaxError[ @/{path-to-my-project}/test/config_auth_spec.js ]: Unexpected token 'const' at compileSourceTextModule (node:internal/modules/esm/utils:339:16) at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:168:18) at callTranslator (node:internal/modules/esm/loader:428:14) at ModuleLoader.moduleProvider (node:internal/modules/esm/loader:434:30)
已用在线代码检查工具排查config_auth_spec.js但未定位问题,完整测试代码如下:
/* ================================= SETUP ================================= */ process.env.NODE_ENV = "testing"; // import * as knexConfig from "../app/config/knexConfig"; import { mockReq, mockRes } from "sinon-express-mock"; import chai from "chai"; import { assert, expect, use, should, request } from "chai"; import { suite, test } from "mocha"; import knexCleaner from "knex-cleaner"; import sinon from "sinon"; import sinonChai from "sinon-chai"; import passport from "passport"; import chaiPassportStrategy from "chai-passport-strategy"; import users from "../db/models/users.js"; import { authConfig } from "../app/config/auth.js"; import { authCtrl } from "../app/controllers/auth.ctrl.js"; const { findUserByEmail, updateUser, user, googleLogin, googleStrategy, jwtStrategy, jwtLogin } = authConfig; const id = "325d0807-1ecf-475b-a5ab-85fea40b3f9e"; const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjMyN…zE1fQ.6y9mMYVXbffHa4Q-aFUd5B3GDyyRF10iBJ28qVlEApk"; const refreshToken = "123"; const name = `firstname lastname}`; const email = "fakeemail@test.com"; const avatar_url = "http://example.com/avatar.png"; const profile = { id, emails: [email], name: { givenName: "firstname", familyName: "lastname" }, picture: avatar_url }; let returnedUser = { id, name: "firstname lastname", email, avatar_url, type: "admin", google_id: "1234", google_token: "5678", created_at: new Date(), updated_at: new Date() }; let req; let doneStub = sinon.stub(); chai.should(); chai.use(sinonChai); chai.use(chaiPassportStrategy); suite("config : auth", function() { afterEach(() => { sinon.restore(); }); suite("use serialize / deserialize", function() { beforeEach(() => { doneStub = sinon.stub(); returnedUser = { id, name: "firstname lastname", email, avatar_url, type: "admin", google_id: "1234", google_token: "5678", created_at: new Date(), updated_at: new Date() }; }); afterEach(() => { sinon.restore(); }); test("serialize returns user Id", async function() { await user.serialize(returnedUser, doneStub); sinon.assert.calledWith(doneStub, null, returnedUser.id); }); test("deserialize returns user", async function() { const getUserByIdStub = sinon .stub(users, "getUserById") .resolves(returnedUser); await user.deserialize(id, doneStub); await getUserByIdStub; sinon.assert.calledWith(doneStub, null, returnedUser); }); test("deserialize handles error if getUserById throws", async function() { const getUserByIdStub = sinon.stub(users, "getUserById").rejects("Error"); await user.deserialize(id, doneStub); getUserByIdStub().catch(err => { sinon.assert.calledWith(doneStub, err, null); }); }); }); suite("findUserByEmail", function() { beforeEach(() => { doneStub = sinon.stub(); }); afterEach(() => { sinon.restore(); }); test("returns user if found user with googleId", async function() { const getUserByEmailStub = sinon .stub(users, "getUserByEmail") .resolves(returnedUser); await authConfig.findUserByEmail(profile, token, doneStub); await getUserByEmailStub(); sinon.assert.calledWith(doneStub, null, returnedUser); }); test("updates user if found user without googleId", async function() { delete returnedUser.google_id; const getUserByEmailStub = sinon .stub(users, "getUserByEmail") .resolves(returnedUser); const updateUserStub = sinon.stub(authConfig, "updateUser").resolves({}); await authConfig.findUserByEmail(profile, token, doneStub); await getUserByEmailStub(); await updateUserStub(); sinon.assert.calledWith( updateUserStub, profile, token, returnedUser.id, doneStub ); }); test("returns null if no user found", async function() { const getUserByEmailStub = sinon .stub(users, "getUserByEmail") .resolves(null); await authConfig.findUserByEmail(profile, token, doneStub); await getUserByEmailStub(); sinon.assert.calledWith(doneStub, null, null); }); test("handles error if getUserByEmail throws", async function() { const getUserByEmailStub = sinon .stub(users, "getUserByEmail") .rejects("Error"); await authConfig.findUserByEmail(profile, token, doneStub); getUserByEmailStub().catch(err => { sinon.assert.calledWith(doneStub, err, null); }); }); }); suite("updateUser", function() { beforeEach(() => { doneStub = sinon.stub(); }); afterEach(() => { sinon.restore(); }); test("updates user and returns done", async function() { const updateUserStub = sinon .stub(users, "updateUser") .resolves(returnedUser); await authConfig.updateUser(profile, token, id, doneStub); await updateUserStub(); sinon.assert.calledWith(doneStub, null, returnedUser); }); test("handles error if updateUser throws", async function() { const updateUserStub = sinon.stub(users, "updateUser").rejects("Error"); await authConfig.updateUser(profile, token, id, doneStub); updateUserStub().catch(err => { sinon.assert.calledWith(doneStub, err, null); }); }); }); suite("jwtLogin", function() { beforeEach(() => { doneStub = sinon.stub(); req = {}; }); afterEach(() => { sinon.restore(); }); test("gets user by id and returns done", async function() { const getUserByIdStub = sinon .stub(users, "getUserById") .resolves(returnedUser); await authConfig.jwtLogin(req, { id }, doneStub); await getUserByIdStub(); sinon.assert.calledWith(doneStub, null, returnedUser); assert.equal(req.user, returnedUser); }); test("handles error if getUserById throws", async function() { const getUserByIdStub = sinon.stub(users, "getUserById").rejects("Error"); await authConfig.jwtLogin({}, { id }, doneStub); getUserByIdStub().catch(err => { sinon.assert.calledWith(doneStub, err, null); }); }); }); suite("googleLogin", function() { beforeEach(() => { doneStub = sinon.stub(); req = { user: returnedUser }; }); afterEach(() => { sinon.restore(); }); test("looks up user by email if no user in req, handles error if findUserByEmail throws", async function() { delete req.user; const findUserByEmailStub = sinon .stub(authConfig, "findUserByEmail") .rejects("Error"); try { await authConfig.googleLogin( req, token, refreshToken, profile, doneStub ); } catch (err) { // console.log(err); } sinon.assert.calledOnce(findUserByEmailStub); }); test("returns user if found", async function() { try { await authConfig.googleLogin( req, token, refreshToken, profile, doneStub ); } catch (err) { console.log(err); } sinon.assert.calledWith(doneStub, null, returnedUser); }); }); });
所有测试在升级前可正常通过,请问如何获取有效错误信息定位问题,或需修改哪些配置让Node.js/Mocha正常运行测试?
解决方案
一、获取更详细的错误信息
- 直接用Node.js运行测试文件:跳过Mocha,执行
node test/config_auth_spec.js,Node.js会输出带行号的详细语法错误,这是最快的定位方式。 - 调整Mocha日志级别:运行测试时添加
--trace-warnings和--verbose参数,例如mocha --trace-warnings --verbose test/config_auth_spec.js,强制输出完整错误栈和细节。
二、配置调整
- 移除实验性解析器选项:Node.js 20+已默认支持ESM文件解析,无需
experimental-specifier-resolution=node,将Mocha配置改为:"mocha": { "extension": ["js"], "spec": "test/**/*.spec.js" } - 检查所有导入路径:ESM要求导入必须带完整扩展名(如
.js),确认项目中所有import语句的路径都包含.js,包括测试文件和被测试的源码文件。 - 确认依赖兼容ESM:检查
sinon-express-mock、chai-passport-strategy等老依赖是否支持ESM。若不兼容,可尝试添加--experimental-modules参数运行Mocha,或替换为ESM兼容的替代包。
三、代码层面修正
- 修复模板字符串语法错误:代码中
const name =firstname lastname}存在多余的闭合花括号`}`,改为`const name = `firstname lastname。 - 修正Chai导入:Chai 5.x核心不包含
request,request属于chai-http模块。若仍需使用,需单独安装chai-http并导入,否则移除import { ..., request } from "chai";中的request。 - 优化异步测试错误处理:部分测试用
try/catch掩盖了错误,确保异步测试的done回调正确调用,或通过async/await的返回值直接抛出错误。
内容的提问来源于stack exchange,提问作者rg_
相关产品推荐
相关产品推荐

