loading...
Bitovi

Testing Loading States using RxJS operators

likeomgitsfeday profile image Jennifer Wadella ・2 min read

A very common pattern is showing some sort of loading visual while data is being fetched. In Angular we can elegantly build this using a reactive programming approach with RxJS - but how do we test it?

Let's say we are fetching a list of our cats names from a service and want to handle loading behavior while that request is made. We might do something like this:

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: `
    <div class="pending" *ngIf="(cats | async)?.isLoading; else loaded"></div>
    <ng-container #loaded>
        <div class="cat" *ngFor="let cat of (cats | async)?.value">
        <p>Name: {{cat.name}}</p>
        </div>
    </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
     })
  }
}

We're using the startWith operator to set our observable to initially have an empty array and and isLoading value of true. In our unit test, we'll make sure our UI is reflecting the loading state as we'd expect:

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

Because I want to first test the isLoading state I want to be able to see what the UI looks like before my getCats method, so I wrap my assertion in a fakeAsync function. This function creates a fake async zone where I can call a tick function to simulate the passage of time. By doing this I essentially can test my Observables as though they were synchronous.

I call tick and fixture.detectChanges for each "timer"; to trigger the component lifecycle like ngOnInit, when the observable is created, when the observable is subscribed to using the async pipe in the view, etc.

Posted on Sep 5 '19 by:

likeomgitsfeday profile

Jennifer Wadella

@likeomgitsfeday

Force of nature. Angular lead @ Bitovi. International & keynote tech speaker. Foodie. #kcnative. Community organizer and WiT advocate.

Bitovi

Business requirements go in — High-quality software comes out. Fortune 5 and startups trust us to make their life easier. Senior UX and JavaScript experts.

Discussion

markdown guide
 

Hi Jennifer, great post. Wouldn't this call the Observable twice though?

    <div class="pending" *ngIf="(cats | async)?.isLoading; else loaded"></div>
    <ng-container #loaded>
        <div class="cat" *ngFor="let cat of (cats | async)?.value">
        <p>Name: {{cat.name}}</p>
        </div>
    </ng-container>

Once for the *ngIf and another time for the *ngFor?

 

Great post! little disappointing that cats need to be served in a RestaurantComponent though :(

 

haha, nice catch. I was transposing from another project code. fixed.