单元测试 valueChanges 可观察管道
Unit test valueChanges observable pipeline
场景
- A
LoginPageComponent
期待用户输入。本次输入为6个字符的键(词组)
- 一旦用户输入了 8 个字符,加载状态将被设置为忙碌。加载完成时,状态将是成功或失败。
- 在失败状态下,会出现一条错误消息。
- 当密钥有效时,用户将被定向到仪表板。
我想测试什么?
- 加载中状态为忙
- 加载失败时状态错误
AuthService
仅使用 6 个字符的长键调用
我的问题是什么?
时间。
如何模拟满足我需求的输入(知道 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()
方法时间操作没问题!
场景
- A
LoginPageComponent
期待用户输入。本次输入为6个字符的键(词组) - 一旦用户输入了 8 个字符,加载状态将被设置为忙碌。加载完成时,状态将是成功或失败。
- 在失败状态下,会出现一条错误消息。
- 当密钥有效时,用户将被定向到仪表板。
我想测试什么?
- 加载中状态为忙
- 加载失败时状态错误
AuthService
仅使用 6 个字符的长键调用
我的问题是什么?
时间。
如何模拟满足我需求的输入(知道 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()
方法时间操作没问题!