You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何测试含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测试方式还出现了超时错误。

解决方案

问题根源

  1. TestBed配置错误:在providers中使用了未赋值的externalService变量,应该用创建好的externalServiceSpy。
  2. mock时机太晚:userRoles$是在AuthService实例化时立即初始化的,此时fetchUserRoles已经被调用,之后再spyOn该方法不会对userRoles$产生影响。
  3. 原服务存在逻辑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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 08:57:34