ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

如何用 Vitest 的 vi.when 按不同参数让 mock 函数返回不同结果

如何用 Vitest 的 vi.when 按不同参数让 mock 函数返回不同结果 如何用 Vitest 的 vi.when 按不同参数让 mock 函数返回不同结果【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest当一个 spy 需要针对不同的调用参数返回不同结果时mockReturnValue帮不上忙因为它对所有调用返回同一个值。旧的做法是用mockImplementation手写参数判断db.findById.mockImplementation((id) { if (id 1) { return Promise.resolve({ id: 1, name: Ella }) } if (id 2) { return Promise.resolve({ id: 2, name: Gracie }) } return Promise.resolve(undefined) })参数一多这类 if/else 链会迅速变得难读。Vitest 5.0.0 起提供的vi.when把参数匹配交给框架你只需声明“匹配什么参数、匹配后做什么”Vitest 在匹配时自动处理参数比较。使用前提有两个Vitest 版本为 5.0.0 或更高vi.when、配套的toHaveBeenExhausted断言均标注Version5.0.0/Version操作对象是一个 spy即通过vi.fn()或vi.spyOn创建的 mock 函数。基本用法calledWith 声明参数then* 声明动作vi.when(spy)返回一个When链对象。链上先调用.calledWith(...args)声明要匹配的参数这创建一个behavior再调用一个then*方法挂上action决定匹配后 spy 的行为。参数按深度相等deep equality比较并支持非对称匹配器如expect.any()。以下示例在 Vitest 的发布说明中完整出现可直接放进测试文件运行import { expect, test, vi } from vitest test(returns user data, async () { const findById vi.fn() vi.when(findById) .calledWith(1) .thenResolve({ id: 1, name: Ella }) .calledWith(2) .thenResolve({ id: 2, name: Gracie }) .calledWith(expect.any(Number)) .thenReject(new Error(not found)) await expect(findById(1)).resolves.toEqual({ id: 1, name: Ella }) await expect(findById(3)).rejects.toThrow(not found) })多个 behavior 可以串在同一条链上。behavior 之间按先注册先匹配first-in-first-out的顺序判断第一个参数匹配的 behavior 获胜类似一串 if/else 语句。所以上例中expect.any(Number)必须放在最后否则它会先匹配所有数字。可用的 then* 动作then*覆盖了 mock 的全部结果类型各自等价于一个mock*方法对照表来自 Conditional Mocking 配方动作等价于等价代码thenReturn(value)mockReturnValue(value)return valuethenThrow(error)mockThrow(error)throw errorthenResolve(value)mockResolvedValue(value)return Promise.resolve(value)thenReject(error)mockRejectedValue(error)return Promise.reject(error)带Once的简写形式thenReturnOnce、thenThrowOnce、thenResolveOnce、thenRejectOnce等价于传{ times: 1 }即该 action 只处理一次调用。同一 behavior 上叠加多个 action一个 behavior 可以挂多个 action。匹配命中时action 按后注册先执行last-in-first-out的顺序被_消耗_最近注册的 action 先运行消耗完后 Vitest 回落到上一个。times选项限制一个 action 能处理多少次调用超过后落到下一个 action不带times的 action 无限次生效。由于 action 按注册逆序评估无限 action 应该先注册这样后面注册的一次性 action 才能在有限时间内临时覆盖它。配方文档中的重试示例import { test, vi } from vitest import { readConfig } from ./config.ts test(retries after an initial failure, async () { const fetchInstance vi.fn() Promiseunknown() vi.when(fetchInstance) .calledWith(/data/config.json) .thenResolve(new Response({ debug: true })) // ↳ indefinite fallback .thenReject(new Error(network error), { times: 1 }) // ↳ applied first and consumed after one call await expect(readConfig(fetchInstance)).resolves.toEqual({ debug: true }) expect(fetchInstance).toHaveBeenCalledTimes(2) })效果是第一次调用返回被拒绝的 Promisetimes: 1被消耗一次后退出第二次调用回落到无限的thenResolve。expect(fetchInstance).toHaveBeenCalledTimes(2)验证了“先失败、重试后成功”这一行为链。后文配方示例如readConfig、sendEmail、getUserById、loadDashboard引用的是各自项目里的被测函数与类型如FindById实际使用时替换为你项目中的对应实现vi.when的链式写法本身保持不变。用非对称匹配器按参数“形状”匹配当你关心的是参数的类型或形状而不是精确值时calledWith支持 非对称匹配器test(sends email to each recipient, () { vi.when(sendEmail) .calledWith(expect.stringContaining()) .thenReturn({ ok: true, message: sent via external relay }) })结合前面说的“behavior 按先注册先匹配”具体匹配器必须注册在宽泛匹配器之前宽泛的才能充当兜底test(sends email to each recipient, () { vi.when(sendEmail) .calledWith(expect.stringContaining(internal.example.com)) .thenReturn({ ok: true, message: sent via internal relay }) .calledWith(expect.stringContaining()) .thenReturn({ ok: true, message: sent via external relay }) })注册顺序陷阱behavior 合并这里有一条容易踩坑的规则注册新 behavior 时Vitest 按注册顺序检查已有 behavior如果新参数已经能匹配某个已有 behavior新的 action 会合并进那个已有 behavior而不是新建一个。vi.when(getRole) .calledWith(expect.any(String)) .thenReturn(user) .calledWith(adminexample.com) .thenReturnOnce(admin)adminexample.com已经匹配expect.any(String)所以第二次注册被合并进去实际效果等价于vi.when(getRole) .calledWith(expect.any(String)) .thenReturn(user) .thenReturnOnce(admin)结果是任何字符串的第一次调用都返回admin而不是只有adminexample.com命中expect(getRole(userexample.com)).toBe(admin) expect(getRole(userexample.com)).toBe(user)如果你确实需要“特定参数一个临时行为、其余参数另一个兜底”把临时行为写成该宽泛 behavior 的叠加 action利用后注册先执行的顺序更可靠而不是指望用另一个具体值新建 behavior。处理没有匹配到任何 behavior 的调用默认情况下spy 被未注册过的参数调用时会回落到 spy 的原始实现如果 spy 没有原始实现返回undefined。vi.when(spy, options)的options.onUnmatched提供三种替代方式1.onUnmatched: throw—— 未注册参数直接抛错。错误类型和文案是固定的不能自定义但消息里包含未匹配的实参便于定位vi.when(db.findById, { onUnmatched: throw }) .calledWith(1) .thenResolve({ id: 1, name: Ella }) await expect(db.findById(1)).resolves.toMatchObject({ name: Ella }) await expect(db.findById(3)).rejects.toThrow( vi.when: no behavior defined when called with [3], )2. 传一个函数—— 未匹配时调用你的函数参数与 spy 相同返回值直接作为 spy 的结果。适合共享 mock 需要按测试不同兜底的场景函数抛错或返回被拒绝的 Promise 时错误会像普通 action 一样传播给调用方const db { findById: vi.fnFindById() } test(returns a placeholder for unknown ids, async () { vi.when( db.findById, { onUnmatched: id Promise.resolve({ id, name: User ${id} }) } ) .calledWith(1) .thenResolve({ id: 1, name: Ella }) await expect(db.findById(1)).resolves.toMatchObject({ name: Ella }) await expect(db.findById(42)).resolves.toMatchObject({ name: User 42 }) })3. 宽泛的非对称匹配器兜底—— 把最宽的calledWith放在链尾作为“其他一切”的 fallback它可以返回值、resolve/reject 或抛错vi.when(db.findById) .calledWith(1) .thenResolve({ id: 1, name: Ella }) .calledWith(2) .thenResolve({ id: 2, name: Gracie }) .calledWith(expect.any(Number)) .thenReject(new Error(user not found))用 toHaveBeenExhausted 验证每个行为都被调用过要确认“注册的所有 behavior 都被实际匹配到、action 被消耗”把vi.when返回的对象传给toHaveBeenExhausted同样自 Vitest 5.0.0 可用见 expect APItest(loads both users, async () { const db { findById: vi.fnFindById() } const w vi.when(db.findById) .calledWith(1) .thenResolveOnce({ id: 1, name: Ella }) .calledWith(2) .thenResolveOnce({ id: 2, name: Gracie }) await loadDashboard(db) expect(w).toHaveBeenExhausted() })如果loadDashboard只调用了findById(1)测试失败错误消息会列出从未匹配到的 behavior文档示例输出AssertionError: expected all behaviors to have been exhausted, but some remain: calledWith(2) ✗ thenReturn({ id: 2, name: Gracie }) never called两条使用边界没有任何 behavior 的vi.when链永远不被视为 exhausted裸.calledWith()而没有then*也一样都会让断言失败无限 action不带times被调用过一次即满足 exhausted 条件之后仍可继续响应。用 using 自动恢复 spyvi.when支持 Explicit Resource Management 协议。用using声明这条链behavior 的作用域就被限制在当前块内离开块时自动恢复 spy 的原始实现需要运行环境支持该协议否则仍用const声明const spy vi.fn(() original) test(with mocked behavior, () { using w vi.when(spy).calledWith(hello).thenReturn(mocked) expect(spy(hello)).toBe(mocked) }) // ← restored here test(without mocked behavior, () { expect(spy(hello)).toBe(original) })小结同一参数的多个返回值用vi.when(spy).calledWith(args).then*()链声明替代mockImplementation里的手写 if/elsebehavior 按先注册先匹配因此具体的放在宽泛的前面action 按后注册先消耗因此无限兜底先注册、times有限的临时行为后注册未匹配调用默认回落到原始实现onUnmatched: throw或onUnmatched: fn可改成抛错或自定义兜底expect(w).toHaveBeenExhausted()用于验证所有注册行为都被真正触发失败时消息会列出未匹配的行为using声明可在测试块结束时自动还原 spy避免污染后续用例。这些能力均要求 Vitest 5.0.0。完整示例见 Conditional Mocking 配方API 细节见vi.when与toHaveBeenExhausted。如果你只需要判断某个值是不是When链例如在工具函数里收窄类型可以用配套的vi.isWhenChain。【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表