Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.8k views
in Technique[技术] by (71.8m points)

angular - Unit test valueChanges observable pipeline

Scenario

  • A LoginPageComponent expect a user input. This input is a key (phrase) of 6 characters.
  • As soon the user typed 8 characters, a loading state will be set to busy. When the loading completes the state will either be success or failed.
  • On failed state, an error message appears.
  • When the key is valid, the user will be directed to the dashboard.

What do I want to test?

  • Loading state is busy while loading
  • Loading state is error when failed
  • AuthService is only called with 6 character long keys

What is my problem?

Time.
How do I simulate an input (which is aware of the debounceTime) that serves my needs? Also the AuthService needs some asynchronous time to check the key, so I can't directly Assert. I can't also subscribe to the observable chain, because it is not public.

Code

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;
  }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

I finally got it. I was thinking to much about the new TestScheduler and marble testing. But this was no the way to go. Instead fakeAsync from Zone.js fits very well:

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);
  });
});

With the tick() method time manipulation was no problem!


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

2.1m questions

2.1m answers

60 comments

56.8k users

...