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

升级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,强制输出完整错误栈和细节。

二、配置调整

  1. 移除实验性解析器选项:Node.js 20+已默认支持ESM文件解析,无需experimental-specifier-resolution=node,将Mocha配置改为:
    "mocha": {
      "extension": ["js"],
      "spec": "test/**/*.spec.js"
    }
    
  2. 检查所有导入路径:ESM要求导入必须带完整扩展名(如.js),确认项目中所有import语句的路径都包含.js,包括测试文件和被测试的源码文件。
  3. 确认依赖兼容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_

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 11:34:56