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断言不生效?如何针对类实现此报错断言功能?
原因分析
- 断言写法错误:
toThrowError断言的目标必须是一个会抛出错误的函数,而不是函数执行后的结果。直接写expect(new Config(...))相当于先完成了实例化操作,把Config实例传给了expect,而非传递一个能触发实例化的函数,因此Vitest报错“expected Config{} to be a function”。 - 异步逻辑的隐藏问题:
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
相关产品推荐
相关产品推荐

