LoginPageComponent
expect a user input. This input is a key (phrase) of 6 characters.AuthService
is only called with 6 character long keysTime.
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.
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;
}
}
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!