使用 RxJS 操作符测试加载状态
一种非常常见的做法是在获取数据时显示某种加载动画。在 Angular 中,我们可以使用 RxJS 的响应式编程方法优雅地实现这一点——但是我们该如何测试它呢?
假设我们要从某个服务获取猫咪名字列表,并且希望在请求发出期间处理加载行为。我们可以这样做:
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
interface ResponseData<T> {
data: Array<T>;
}
interface MappedData<T> {
value: Array<T>;
isLoading: boolean;
}
@Component({
selector: 'cat-list',
template: `
<ng-container *ngIf="cats$ | async as cats">
<div class="pending" *ngIf="cats?.isLoading; else loaded"></div>
<ng-template #loaded>
<div class="cat" *ngFor="let cat of cats.value">
<p>Name: {{cat.name}}</p>
</div>
</ng-template>
</ng-container>
`,
styleUrls: ['./cat.component.less']
})
export class CatListComponent implements OnInit {
public cats$: Observable<MappedData<Cat>>;
constructor(private catService: CatService) { }
ngOnInit() {
this.cats$ = this.catService.getCats().pipe(
map((res: ResponseData<Cat>) => {
return {
value: res.data,
isLoading: false
}
}),
startWith({
value: [],
isLoading: true
})
}
}
我们使用该startWith操作符将可观察对象初始设置为空数组,并将 isLoading 值设置为 true。在单元测试中,我们将确保 UI 能够按预期反映加载状态:
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { CatListComponent } from './cat-list.component';
import { of, asyncScheduler } from 'rxjs';
import { CatsService } from '../catList/cats.service';
class MockCatsService {
getCats() {
return of({
data: [{
name: 'Sake',
age: 10
},
{
name: 'Butter',
age: 15
},
{
name: 'Parker',
age: 7
},
{
name: 'Kaylee',
age: 2
}]
}, asyncScheduler);
}
}
describe('CatListComponent', () => {
let component: CatListComponent;
let fixture: ComponentFixture<CatListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CatListComponent ],
providers: [{
provide: CatsService,
useClass: MockCatsService
}],
})
.compileComponents();
}));
it('should create', () => {
const fixture = TestBed.createComponent(CatListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
expect(component).toBeTruthy();
fixture.destroy();
});
it('should show loading div while results are loading', fakeAsync((): void => {
const fixture = TestBed.createComponent(CatListComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
const loadingDiv = compiled.querySelector('.loading');
expect(loadingDiv).toBeTruthy();
fixture.destroy();
}));
it('should show cat divs when results have loaded', fakeAsync((): void => {
const fixture = TestBed.createComponent(CatListComponent);
fixture.detectChanges();
tick();
fixture.detectChanges();
tick();
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
const loadingDiv = compiled.getElementsByClassName('cat');
expect(loadingDiv.length).toBe(4);
fixture.destroy();
}));
});
因为我首先想测试isLoading状态,所以希望在执行方法之前能够看到用户界面的样子getCats,因此我将断言封装在一个fakeAsync函数中。这个函数创建了一个模拟的异步区域,我可以在其中调用一个tick函数来模拟时间的流逝。通过这种方式,我实际上可以像测试同步对象一样测试我的 Observable。
我为每个“计时器”调用tick` fixture.detectChangestimer`,以触发组件生命周期,例如ngOnInit,当可观察对象创建时,当使用视图中的异步管道订阅可观察对象时等等。