Angular Jasmine测试中如何为不同泛型的同类依赖配置独立Mock Provider
TypeScript的泛型属于编译期类型检查机制,代码编译为JavaScript后所有泛型标记会被完全擦除,不存在运行时层面的Store<LanguageState>和Store<UserState>类型差异。
Angular的依赖注入是基于运行时token做匹配的,你之前写的两个带泛型的provider,在运行时本质都是同一个Store类作为注入标识,Angular DI容器会自动用后注册的配置覆盖同token的前置配置,两个组件依赖最终拿到的其实都是最后一个provider对应的mock,自然无法实现独立隔离。
按实现复杂度从低到高排序,都是经过实战验证的写法:
方案1:直接替换组件私有依赖,零侵入最灵活
这个方法完全不用调整providers的注入配置,拿到组件实例后直接把两个Store的私有属性替换成独立mock即可:
// 先创建两个完全独立的spy实例,各自的逻辑互不影响 const languageStoreMock = jasmine.createSpyObj('languageStore', ['pipe']); const userStoreMock = jasmine.createSpyObj('userStore', ['pipe']); beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [YourComponent], // 这里给Store提供一个通用占位mock就行 providers: [ { provide: Store, useValue: jasmine.createSpyObj('Store', ['pipe']) } ] }).compileComponents(); const fixture = TestBed.createComponent(YourComponent); const component = fixture.componentInstance; // 直接替换组件内部的两个Store私有属性为你的独立mock (component as any).languageStore = languageStoreMock; (component as any).userStore = userStoreMock; // 后续就可以分别给两个mock配置独立的返回逻辑、做调用断言 languageStoreMock.pipe.and.returnValue(of(mockLanguageData)); userStoreMock.pipe.and.returnValue(of(mockUserData)); });
优点是完全不受DI顺序、泛型擦除的影响,修改成本极低,是单测场景下最常用的写法。
方案2:自定义工厂按构造函数顺序匹配独立Mock
如果你希望从DI注入阶段就拿到独立实例,可以利用Angular实例化组件时按构造函数参数顺序调用工厂函数的特性,通过闭包计数器跟踪调用次数,按顺序返回对应的mock:
const languageStoreMock = jasmine.createSpyObj('languageStore', ['pipe']); const userStoreMock = jasmine.createSpyObj('userStore', ['pipe']); TestBed.configureTestingModule({ providers: [ { provide: Store, useFactory: (() => { let injectCount = 0; // 每次DI系统请求Store实例时,就会调用这个内部函数 return () => { injectCount++; // 对应构造函数第一个参数:languageStore if (injectCount === 1) return languageStoreMock; // 对应构造函数第二个参数:userStore if (injectCount === 2) return userStoreMock; // 兜底返回通用mock,避免其他地方注入Store时报错 return jasmine.createSpyObj('DefaultStore', ['pipe']); } })() } ] })
注意:这个方法的计数逻辑必须和组件构造函数里的Store参数顺序严格对应,后续调整构造函数依赖顺序时,要同步修改工厂的计数规则,否则会出现mock错配的问题。
方案3:用专属Injection Token标记依赖,长期维护最规范
如果可以小幅修改业务组件代码,建议从根源上解决泛型擦除的问题,给每个Store依赖定义独立的注入token,替代直接用Store类做注入标识:
// 在language store的状态文件中定义专属token export const LANGUAGE_STORE = new InjectionToken<Store<LanguageState>>('LanguageStore'); // 在user store的状态文件中定义专属token export const USER_STORE = new InjectionToken<Store<UserState>>('UserStore'); // 组件构造函数用@Inject标记对应的token constructor( @Inject(LANGUAGE_STORE) private readonly languageStore: Store<LanguageState>, @Inject(USER_STORE) private readonly userStore: Store<UserState> ) {}
对应的spec配置就可以完全按照你最初的想法实现独立provider:
providers: [ { provide: LANGUAGE_STORE, useValue: languageStoreMock }, { provide: USER_STORE, useValue: userStoreMock } ]
这个方法没有顺序依赖、不存在token覆盖问题,稳定性最高,适合长期维护的大型项目。
你之前尝试的通过callFake判断pipe入参来源的方法,不建议用于生产级单测:
- @ngrx/store的
select函数返回的是封装后的操作符函数,要反向解析内部选择器参数需要额外写适配逻辑,复杂度很高 - 一旦后续在pipe中加入其他操作符(比如
distinctUntilChanged、debounceTime)、或者ngrx版本升级调整select的实现,整个判断逻辑就会失效 - 两个依赖共用同一个mock实例,无法单独对某个Store的调用次数、调用参数做精确断言
内容的提问来源于stack exchange,提问作者Stacker-flow

