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

Vitest类初始化断言报错原因及实现方案咨询

类实例化的Vitest断言问题:错误捕获失败的原因与解决方法

问题详情

测试用例代码

it("should throw an error if the config file is not found but the path is valid", () => {
  expect(new Config("./tests/etech-ui.config.ts")).toThrowError();
});

执行测试时的错误信息

FAIL  packages/etech-ui-utils/src/tests/Config.spec.ts > parseConfig > should throw an error if the config file is not found but the path is valid
AssertionError: expected Config{} to be a function
 ❯ packages/etech-ui-utils/src/tests/Config.spec.ts:26:54
     24| 
     25|   it("should throw an error if the config file is not found but the path is valid", () => {
     26|     expect(new Config("./tests/etech-ui.config.ts")).toThrowError();
       |                                                      ^
     27|   });

Config类相关定义

export default class Config {
  private config: ConfigT | ConfigT[];
  constructor(path: string) {
    this.getConfigContents(path);
  }

  public async getConfigContents(path: string): Promise<ConfigT[] | ConfigT> {
    const pathRegex = /^(\/?(\.{1,2}\/)*[a-zA-Z0-9\-.]+)+$/;

    if (!pathRegex.test(path)) {
      throw new Error("The path to the etech.config.ts file is invalid.");
    }

    let foundValidPath = false;
    let configExtension = "";

    let i = 0;

    while (i < path.split(".").length + 1) {
      if (i >= path.split(".").length && !foundValidPath) {
        throw new Error("Your etech-ui.config file must end in .ts, .js, .mjs, .cjs, .mts, or .cts.");
      }

      if (
        path.split(".")[i] === "ts" ||
        path.split(".")[i] === "js" ||
        path.split(".")[i] === "mjs" ||
        path.split(".")[i] === "cjs" ||
        path.split(".")[i] === "mts" ||
        path.split(".")[i] === "cts"
      ) {
        console.log(path.split(".")[i]);
        foundValidPath = true;
        configExtension = path.split(".")[i];
      }

      i++;
    }

    try {
      await readFile(path, "utf8");
    } catch (err: any) {
      if (err.code === "ENOENT") {
        throw new Error("Could not find the etech.config.ts file.");
      }
    }

    this.config = await import(path);
    return this.config;
  }
}

用户疑问:为何该expect断言不生效?如何针对类实现此报错断言功能?


原因分析

  1. 断言写法错误:toThrowError断言的目标必须是一个会抛出错误的函数,而不是函数执行后的结果。直接写expect(new Config(...))相当于先完成了实例化操作,把Config实例传给了expect,而非传递一个能触发实例化的函数,因此Vitest报错“expected Config{} to be a function”。
  2. 异步逻辑的隐藏问题:getConfigContents是异步方法,在constructor中直接调用时,由于constructor不能标记为async,异步错误会变成未捕获的Promise拒绝,而非同步抛出的错误——就算修正了断言写法,同步的toThrowError也无法捕获这类异步错误。

解决方案

步骤1:修正断言的基本写法

把实例化操作包装成匿名函数传给expect,让Vitest在调用函数时捕获错误:

it("should throw an error if the config file is not found but the path is valid", () => {
  expect(() => new Config("./tests/etech-ui.config.ts")).toThrowError();
});

但这只能解决同步错误的捕获,还需处理异步逻辑的问题。

步骤2:修复Config类的异步初始化逻辑

由于constructor无法处理异步操作,有两种常见解决方式:

方案A:将初始化逻辑改为同步(适合不需要异步的场景)

把getConfigContents改成同步方法,替换异步API为同步版本:

import { readFileSync } from 'fs';

export default class Config {
  private config: ConfigT | ConfigT[];
  constructor(path: string) {
    this.getConfigContents(path);
  }

  public getConfigContents(path: string): ConfigT[] | ConfigT {
    const pathRegex = /^(\/?(\.{1,2}\/)*[a-zA-Z0-9\-.]+)+$/;

    if (!pathRegex.test(path)) {
      throw new Error("The path to the etech.config.ts file is invalid.");
    }

    let foundValidPath = false;
    let configExtension = "";

    let i = 0;

    while (i < path.split(".").length + 1) {
      if (i >= path.split(".").length && !foundValidPath) {
        throw new Error("Your etech-ui.config file must end in .ts, .js, .mjs, .cjs, .mts, or .cts.");
      }

      if (
        path.split(".")[i] === "ts" ||
        path.split(".")[i] === "js" ||
        path.split(".")[i] === "mjs" ||
        path.split(".")[i] === "cjs" ||
        path.split(".")[i] === "mts" ||
        path.split(".")[i] === "cts"
      ) {
        foundValidPath = true;
        configExtension = path.split(".")[i];
      }

      i++;
    }

    try {
      readFileSync(path, "utf8"); // 同步读取文件
    } catch (err: any) {
      if (err.code === "ENOENT") {
        throw new Error("Could not find the etech.config.ts file.");
      }
    }

    this.config = require(path); // 同步导入模块(需确保TS配置支持CommonJS)
    return this.config;
  }
}

修改后,步骤1的断言就能正常捕获同步抛出的错误。

方案B:使用静态异步工厂方法(保留异步逻辑)

不在constructor中执行异步操作,而是通过静态方法创建实例,用async/await处理异步流程:

export default class Config {
  private config: ConfigT | ConfigT[];
  
  // 私有构造函数,禁止直接实例化
  private constructor() {}

  // 静态工厂方法,负责异步初始化
  public static async create(path: string): Promise<Config> {
    const instance = new Config();
    await instance.getConfigContents(path);
    return instance;
  }

  // 改为私有方法,仅内部调用
  private async getConfigContents(path: string): Promise<ConfigT[] | ConfigT> {
    // 保留原有的异步逻辑
    const pathRegex = /^(\/?(\.{1,2}\/)*[a-zA-Z0-9\-.]+)+$/;

    if (!pathRegex.test(path)) {
      throw new Error("The path to the etech.config.ts file is invalid.");
    }

    let foundValidPath = false;
    let configExtension = "";

    let i = 0;

    while (i < path.split(".").length + 1) {
      if (i >= path.split(".").length && !foundValidPath) {
        throw new Error("Your etech-ui.config file must end in .ts, .js, .mjs, .cjs, .mts, or .cts.");
      }

      if (
        path.split(".")[i] === "ts" ||
        path.split(".")[i] === "js" ||
        path.split(".")[i] === "mjs" ||
        path.split(".")[i] === "cjs" ||
        path.split(".")[i] === "mts" ||
        path.split(".")[i] === "cts"
      ) {
        foundValidPath = true;
        configExtension = path.split(".")[i];
      }

      i++;
    }

    try {
      await readFile(path, "utf8");
    } catch (err: any) {
      if (err.code === "ENOENT") {
        throw new Error("Could not find the etech.config.ts file.");
      }
    }

    this.config = await import(path);
    return this.config;
  }
}

对应的测试用例要改成异步测试,用rejects.toThrowError捕获Promise的拒绝:

it("should throw an error if the config file is not found but the path is valid", async () => {
  await expect(Config.create("./tests/etech-ui.config.ts")).rejects.toThrowError();
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 13:28:10