如何测试含Observable与shareReplay的Angular 16服务?
问题描述
我有如下Angular 16服务:
@Injectable({ providedIn: 'root' }) export class AuthService { private readonly userRoles$: Observable<string[]> = this.fetchUserRoles().pipe(shareReplay(1)); //<-- get user info only once constructor(private readonly externalService: ExternalService) {} fetchUserRoles(): Observable<string[]> { return new Observable<string[]>((observer) => { this.externalService.fetchUserInfo((error, claims) => { let userRoles: string[] = []; if (error) { console.error( 'An error has occurred', error ); throw new Error(error.message); } const authorizations = claims.user_authorization.find((auth) => auth.resource === 'MY_APP')?. .permissions.map((permission) => permission.name) ?? []; observer.next(userRoles); }); }); } isUserAuthorized$(): Observable<boolean> { return this.userRoles$.pipe(map((roles) => roles.length > 0)); } }
想要测试isUserAuthorized$()方法,编写了如下测试类:
describe('AuthService', () => { let externalService: jasmine.SpyObj<ExternalService>; let authService: AuthService; beforeEach(() => { const externalServiceSpy = jasmine.createSpyObj('ExternalService', [ 'fetchUserInfo' ]); TestBed.configureTestingModule({ providers: [ { provide: ExternalService, useValue: externalService }, AuthService ] }); externalService = TestBed.inject( ExternalService ) as jasmine.SpyObj<ExternalService>; authService = TestBed.inject(AuthService); }); it('Should mark user as authorized because he has ADMIN role', fakeAsync(() => { externalService.fetchUserInfo.and.stub(); spyOn(authService, 'fetchUserRoles').and.returnValue(of(['ADMIN'])); let result; authService.isUserAuthorized$().subscribe((value) => (result = value)); tick(); expect(result).toBeTrue(); })); });
但持续收到错误:Expected undefined to be true.,猜测是服务实例化后才mockfetchUserRoles方法导致的问题,尝试async/await测试方式还出现了超时错误。
解决方案
问题根源
- TestBed配置错误:在
providers中使用了未赋值的externalService变量,应该用创建好的externalServiceSpy。 - mock时机太晚:
userRoles$是在AuthService实例化时立即初始化的,此时fetchUserRoles已经被调用,之后再spyOn该方法不会对userRoles$产生影响。 - 原服务存在逻辑bug:
fetchUserRoles中最终observer.next(userRoles),但userRoles始终是空数组,正确应该返回处理后的authorizations。
修正后的测试代码
describe('AuthService', () => { let externalService: jasmine.SpyObj<ExternalService>; let authService: AuthService; beforeEach(() => { const externalServiceSpy = jasmine.createSpyObj('ExternalService', [ 'fetchUserInfo' ]); TestBed.configureTestingModule({ providers: [ { provide: ExternalService, useValue: externalServiceSpy }, // 修正:使用创建好的spy对象 AuthService ] }); externalService = TestBed.inject(ExternalService) as jasmine.SpyObj<ExternalService>; authService = TestBed.inject(AuthService); }); it('Should mark user as authorized because he has ADMIN role', fakeAsync(() => { // 模拟externalService.fetchUserInfo的回调,传入包含权限的claims externalService.fetchUserInfo.and.callFake((callback) => { callback(null, { user_authorization: [ { resource: 'MY_APP', permissions: [{ name: 'ADMIN' }] } ] }); }); let result: boolean | undefined; authService.isUserAuthorized$().subscribe((value) => { result = value; }); tick(); // 触发Observable的异步流程 expect(result).toBeTrue(); })); });
额外修正:原服务的逻辑bug
在fetchUserRoles方法中,将observer.next(userRoles)改为observer.next(authorizations),否则无论权限如何,返回的始终是空数组:
// 原代码 observer.next(userRoles); // 修正后 observer.next(authorizations);
可选方案:提前mock fetchUserRoles
如果一定要mockfetchUserRoles,需要在服务实例化前就替换该方法,可以通过useFactory来创建服务:
beforeEach(() => { const externalServiceSpy = jasmine.createSpyObj('ExternalService', ['fetchUserInfo']); TestBed.configureTestingModule({ providers: [ { provide: ExternalService, useValue: externalServiceSpy }, { provide: AuthService, useFactory: (extService: ExternalService) => { const service = new AuthService(extService); // 实例化后立即mock fetchUserRoles spyOn(service, 'fetchUserRoles').and.returnValue(of(['ADMIN'])); return service; }, deps: [ExternalService] } ] }); externalService = TestBed.inject(ExternalService) as jasmine.SpyObj<ExternalService>; authService = TestBed.inject(AuthService); });
内容的提问来源于stack exchange,提问作者Andrei Manolache
相关产品推荐
相关产品推荐

