TypeScript测试中Mock第三方库ioredis遇阻求助
Mock ioredis 在 TypeScript + AVA 环境下的解决方案
我完全懂你这种明明觉得是常规操作却卡壳的挫败感!Mock 第三方库在 TypeScript + AVA 的组合里确实容易踩坑,尤其是像 ioredis 这种导出类/静态子类的库,咱们一步步拆解问题,看看你之前的尝试哪里出了问题,再给你几个靠谱的解决方案。
先说说你之前踩的常见坑点
- 模块加载顺序问题:TypeScript 编译后的模块会被 Node.js 缓存,如果你的包装器在 mock 之前就被导入了,那 mock 根本不会生效——这是大多数人第一次尝试时栽的跟头。
- ioredis 的导出结构:IORedis 不仅有默认导出的 Redis 类,还有静态导出的
Cluster子类,直接 stub 整个模块可能没覆盖到你实际用的部分。 - AVA 的执行环境:AVA 每个测试文件是独立执行的,但模块缓存是全局的,所以需要确保每次测试前清空缓存或者动态导入依赖。
针对不同 Mock 工具的正确用法
1. Sinon 正确姿势
如果你坚持用 Sinon,要注意精准 stub 你用到的类(比如 Cluster),并且确保 mock 在包装器导入前执行:
import * as IORedis from 'ioredis'; import sinon from 'sinon'; import { test } from 'ava'; test('should call IORedis.Cluster', async t => { // 1. 先 stub Cluster 类 const clusterStub = sinon.stub(IORedis, 'Cluster'); // 2. 模拟 Cluster 实例的方法(比如你用到的 get/set) const mockClusterInstance = { get: sinon.stub().resolves('mock-value'), set: sinon.stub().resolves('OK') }; clusterStub.returns(mockClusterInstance); // 3. 动态导入你的包装器(关键!确保 mock 后才加载) const { RedisWrapper } = await import('./homeMadeIoredisWrapperClass'); // 4. 执行测试逻辑 const wrapper = new RedisWrapper(); await wrapper.get('test-key'); // 5. 断言 sinon.assert.calledOnce(clusterStub); t.pass(); // 6. 恢复 stub clusterStub.restore(); });
2. testdouble.js 正确用法
testdouble 的 replace 方法需要注意模块路径,并且同样要动态导入包装器:
import td from 'testdouble'; import { test } from 'ava'; test.beforeEach(async () => { // 1. 创建 mock 的 ioredis 对象,包含你用到的 Cluster const mockIORedis = td.object(['Cluster']); // 2. 模拟 Cluster 构造函数的返回值 const mockCluster = td.object(['get', 'set']); td.when(mockIORedis.Cluster(td.matchers.anything())).thenReturn(mockCluster); // 3. 替换 ioredis 模块 td.replace('ioredis', mockIORedis); }); test('should use mocked Cluster', async t => { // 动态导入包装器 const { RedisWrapper } = await import('./homeMadeIoredisWrapperClass'); const wrapper = new RedisWrapper(); // 执行方法 await wrapper.get('test-key'); // 断言 Cluster 被调用 td.verify(mockIORedis.Cluster(td.matchers.anything())); t.pass(); });
3. mockery 正确用法
mockery 需要配置缓存清理,并且同样要控制导入时机:
import mockery from 'mockery'; import { test } from 'ava'; test.before(() => { // 启用 mockery,关闭未注册模块的警告 mockery.enable({ warnOnUnregistered: false, useCleanCache: true // 每次测试清空模块缓存 }); }); test.beforeEach(() => { // 注册 mock 的 ioredis const mockIORedis = { Cluster: function() { return { get: () => Promise.resolve('mock-value') }; } }; mockery.registerMock('ioredis', mockIORedis); }); test('should mock Cluster successfully', async t => { // 动态导入包装器 const { RedisWrapper } = await import('./homeMadeIoredisWrapperClass'); const wrapper = new RedisWrapper(); const result = await wrapper.get('test-key'); t.is(result, 'mock-value'); }); test.after(() => { mockery.disable(); });
最省心的方案:依赖注入
其实不用跟模块系统较劲,给你的包装器加个依赖注入的口子,测试时直接传 mock 类进去就行——这是最可靠也最符合设计原则的方式:
// 你的包装器代码(修改后) import { Cluster, ClusterOptions } from 'ioredis'; export class RedisWrapper { private cluster: Cluster; // 把 Cluster 类作为可选参数传入,默认用真实的 IORedis.Cluster constructor(clusterClass: typeof Cluster = Cluster, options?: ClusterOptions) { this.cluster = new clusterClass(options || ['redis://localhost:6379']); } async get(key: string) { return this.cluster.get(key); } } // 测试代码(完全不用第三方 mock 工具) import { test } from 'ava'; import { RedisWrapper } from './homeMadeIoredisWrapperClass'; test('should call Cluster constructor', async t => { let clusterConstructorCalled = false; // 自己写个简单的 mock Cluster 类 const mockClusterClass = class MockCluster { constructor() { clusterConstructorCalled = true; } get() { return Promise.resolve('mock-value'); } }; const wrapper = new RedisWrapper(mockClusterClass); await wrapper.get('test-key'); t.true(clusterConstructorCalled); });
这种方式完全避开了模块缓存和第三方工具的兼容性问题,以后扩展或者换测试框架也不用改 mock 逻辑。
你绝对不是在做不可能的事,只是没踩对 mock 的时机或者没注意 TypeScript 模块的加载特性。试试上面的方案,应该能解决你的问题!
内容的提问来源于stack exchange,提问作者Automatico
相关产品推荐
相关产品推荐

