单元测试 valueChanges 可观察管道

Unit test valueChanges observable pipeline

场景

我想测试什么?

我的问题是什么?

时间。
如何模拟满足我需求的输入(知道 debounceTime)?另外 AuthService 需要一些异步时间来检查密钥,所以我不能直接断言。我也不能订阅可观察链,因为它不是 public.

代码

export class LoginPage implements OnInit {
  loadingState = LoaderState.None;
  message: string;
  form: FormGroup = this.formBuilder.group({ key: '' });

  constructor(
    private authService: AuthService,
    private formBuilder: FormBuilder
  ) {}

  ngOnInit(): void {
    this.form.get('key')?.valueChanges.pipe(
        tap(() => (this.loadingState = LoaderState.None)),
        debounceTime(350),
        filter((key: string) => key.length === 8),
        tap(() => (this.loadingState = LoaderState.Loading)),
        switchMap((key: string) =>
          of(key).pipe(
            switchMap(() => this.authService.authenticate(key)),
            catchError((error) => this.handleErrorStatusCode(error))
          )
        ),
        tap(() => (this.loadingState = LoaderState.Done))
      )
      .subscribe((_) => {
        console.log('success'); //TODO: Navigate
      });
  }

  private handleErrorStatusCode(error: any): Observable<never> {
    this.loadingState = LoaderState.Failed;
    // Set error logic...
    return EMPTY;
  }
}

我终于明白了。我想了很多关于新的 TestScheduler 和大理石测试。但这不是办法。相反 Zone.js 中的 fakeAsync 非常适合:

describe('LoginPage', () => {
  let component: LoginPage;
  let mockAuthService: any;
  let fixture: ComponentFixture<LoginPage>;

  beforeEach(async () => {
    mockAuthService = jasmine.createSpyObj(['authenticate']);
    await TestBed.configureTestingModule({
      declarations: [LoginPage],
      imports: [ReactiveFormsModule],
      providers: [{ provide: AuthService, useValue: mockAuthService }]
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(LoginPage);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('is in busy state while loading', fakeAsync(() => {
    mockAuthService.authenticate.and.returnValue(of('result').pipe(delay(100)));

    component.form.patchValue({ token: '123456' });
    tick(250);
    discardPeriodicTasks();

    expect(component.loadingState).toBe(LoaderState.Loading);
  }));

  it('it is in error state when auth service denies', fakeAsync(() => {
    mockAuthService.authenticate.and.returnValue(throwError({ status: 401 }));

    component.form.patchValue({ token: '123456' });
    tick(250);
    expect(component.loadingState).toBe(LoaderState.Failed);
    expect(component.message).toBeDefined();
  }));

  it('is in success state when auth service accept the key', fakeAsync(() => {
    mockAuthService.authenticate.and.returnValue(of('result'));

    component.form.patchValue({ key: '123456' });
    tick(250);
    expect(component.loadingState).toBe(LoaderState.Done);
  }));

  it('resets state on input', fakeAsync(() => {
    mockAuthService.authenticate.and.returnValue(of('token'));

    component.form.patchValue({ key: '123456' });
    tick(250);
    expect(component.loadingState).toBe(LoaderState.Done);

    component.form.patchValue({ key: '12345' });
    tick(250);
    expect(component.loadingState).toBe(LoaderState.Idle);
  }));

  it('should not have error message after construction', () => {
    expect(component.message).toBeNull();
  });

  it('is in idle state after construction', () => {
    expect(component.loadingState).toBe(LoaderState.Idle);
  });
});

使用 tick() 方法时间操作没问题!