Angular Jasmine测试:覆写Keycloak服务私有成员的init方法
问题描述
我有一个Angular服务KeycloakService,它的私有成员是外部npm库keycloak-js中的Keycloak类实例。该服务的构造函数中调用了会发起HTTP请求的keycloak.init()方法。编写单元测试时,我希望避免调用真实的keycloak.init函数,同时保留服务的其他所有方法和属性。
极简代码示例如下:
外部库中的Keycloak类
// 定义在外部npm库中 export class Keycloak { init() { // 发起HTTP请求 console.log('Obj func'); } }
待测试的KeycloakService
@Injectable({ providedIn: 'root', }) export class KeycloakService{ private keycloak; constructor() { this.keycloak = new Keycloak(); this.keycloak.init(); } // 其他方法和属性 }
初始测试代码
describe('KeycloakService', () => { let service: KeycloakService; const objectSpy: jasmine.SpyObj<Keycloak> = jasmine.createSpyObj('Keycloak', [ 'init', ]); beforeEach(() => { TestBed.configureTestingModule({ providers: [KeycloakService], // 如何用keycloakSpy替换KeycloakService.keycloak,同时保留其他方法和属性 }); service = TestBed.inject(KeycloakService); }); it('should be created', () => { expect(service).toBeTruthy(); }); });
我已经通过将Keycloak类包装为Angular服务的方式解决了问题,但仍想了解是否有无需包装的实现方式。
解决方案
方案一:已实现的包装服务方式
通过创建继承自Keycloak的Angular包装服务,利用依赖注入替换实例:
包装类定义
// 定义在外部npm库中 export class Keycloak { init() { // 发起HTTP请求 console.log('Obj func'); } } @Injectable({ providedIn: 'root', }) export class KeycloakWrapper extends Keycloak {}
修改后的KeycloakService
@Injectable({ providedIn: 'root', }) export class KeycloakService { constructor(private keycloak: KeycloakWrapper) {} // 其他方法和属性 }
测试代码
describe('KeycloakService', () => { let service: KeycloakService; const keycloakSpy: jasmine.SpyObj<KeycloakWrapper> = jasmine.createSpyObj( 'KeycloakWrapper', ['init'] ); beforeEach(() => { TestBed.configureTestingModule({ providers: [ KeycloakService, { provide: KeycloakWrapper, useValue: keycloakSpy }, ], }); service = TestBed.inject(KeycloakService); }); it('should be created', () => { expect(service).toBeTruthy(); expect(keycloakSpy.init).toHaveBeenCalled(); // 可选:验证init被调用 }); });
方案二:无需包装的实现方式
如果不想创建包装类,可以通过以下几种方式实现:
方式1:SpyOn Keycloak的构造函数
在测试前拦截Keycloak的构造函数,让它返回预设的spy对象:
describe('KeycloakService', () => { let service: KeycloakService; const keycloakSpy: jasmine.SpyObj<Keycloak> = jasmine.createSpyObj('Keycloak', ['init']); beforeEach(() => { // 拦截Keycloak构造函数,返回spy对象 spyOn(window, 'Keycloak').and.returnValue(keycloakSpy); TestBed.configureTestingModule({ providers: [KeycloakService], }); service = TestBed.inject(KeycloakService); }); it('should be created', () => { expect(service).toBeTruthy(); expect(keycloakSpy.init).toHaveBeenCalled(); }); });
注意:如果
Keycloak不是挂载在window上,而是通过模块导入的,需要调整拦截对象为对应的模块导出。
方式2:用工厂函数手动替换私有成员
通过工厂函数创建KeycloakService实例,手动替换私有成员keycloak:
describe('KeycloakService', () => { let service: KeycloakService; const keycloakSpy: jasmine.SpyObj<Keycloak> = jasmine.createSpyObj('Keycloak', ['init']); beforeEach(() => { TestBed.configureTestingModule({ providers: [ { provide: KeycloakService, useFactory: () => { const serviceInstance = new KeycloakService(); // 用类型断言绕过私有成员访问限制 (serviceInstance as any).keycloak = keycloakSpy; return serviceInstance; } } ], }); service = TestBed.inject(KeycloakService); }); it('should be created', () => { expect(service).toBeTruthy(); // 若需要验证init调用,可手动触发或调整逻辑 }); });
方式3:改造KeycloakService支持工厂注入(推荐长期方案)
微调KeycloakService的构造逻辑,注入Keycloak的工厂函数,测试时替换工厂实现:
@Injectable({ providedIn: 'root', }) export class KeycloakService{ private keycloak; // 注入工厂函数,默认创建新的Keycloak实例 constructor(@Inject('KEYCLOAK_FACTORY') keycloakFactory: () => Keycloak = () => new Keycloak()) { this.keycloak = keycloakFactory(); this.keycloak.init(); } // 其他方法和属性 }
对应的测试代码:
describe('KeycloakService', () => { let service: KeycloakService; const keycloakSpy: jasmine.SpyObj<Keycloak> = jasmine.createSpyObj('Keycloak', ['init']); beforeEach(() => { TestBed.configureTestingModule({ providers: [ KeycloakService, { provide: 'KEYCLOAK_FACTORY', useValue: () => keycloakSpy } ], }); service = TestBed.inject(KeycloakService); }); it('should be created', () => { expect(service).toBeTruthy(); expect(keycloakSpy.init).toHaveBeenCalled(); }); });
这种方式无需额外包装类,同时提升了服务的可测试性,适合长期维护。
内容的提问来源于stack exchange,提问作者cpper
相关产品推荐
相关产品推荐

