Component:
import { Component, OnInit } from "@angular/core"; import { TwainService } from "../twain.service"; import { Observable, of } from "rxjs"; import { startWith, catchError } from "rxjs/operators"; @Component({ selector: "twain", templateUrl: "./twain.component.html", styleUrls: ["./twain.component.scss"] }) export class TwainComponent implements OnInit { errorMessage: string; quote: Observable<any>; constructor(private twainService: TwainService) {} ngOnInit() { this.getQuote(); } getQuote() { this.errorMessage = ""; this.quote = this.twainService.getQuote().pipe( startWith("..."), catchError((err: any) => { // Wait a turn because errorMessage already set once this turn setTimeout(() => (this.errorMessage = err.message || err.toString())); return of("..."); // reset message to placeholder }) ); } }
<p class="twain"> <i>{{ quote | async }}</i> </p> <button (click)="getQuote()">Next quote</button> <p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>
Testing code:
import { async, ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; import { of, throwError } from "rxjs"; import { TwainComponent } from "./twain.component"; import { TwainService } from "../twain.service"; import { cold, getTestScheduler } from "jasmine-marbles"; fdescribe("TwainComponent", () => { let component: TwainComponent; let fixture: ComponentFixture<TwainComponent>; let quoteEl; let testQuote; let getQuoteSpy; beforeEach(() => { testQuote = "Test Quote"; // Create a fake TwainService object with a `getQuote()` spy const twainService = jasmine.createSpyObj("TwainService", ["getQuote"]); // Make the spy return a synchronous Observable with the test data getQuoteSpy = twainService.getQuote.and.returnValue(of(testQuote)); TestBed.configureTestingModule({ declarations: [TwainComponent], providers: [{ provide: TwainService, useValue: twainService }] }); fixture = TestBed.createComponent(TwainComponent); component = fixture.componentInstance; quoteEl = fixture.nativeElement.querySelector(".twain"); }); it("should show quote after getQuote (marbles)", () => { // observable test quote value and complete(), after delay const q$ = cold("-a|", { a: testQuote }); getQuoteSpy.and.returnValue(q$); fixture.detectChanges(); // ngOnInit() expect(quoteEl.textContent).toBe("...", "should show placeholder"); getTestScheduler().flush(); // flush the observables fixture.detectChanges(); // update view expect(quoteEl.textContent).toBe(testQuote, "should show quote"); expect(component.errorMessage).toBe("", "should not show error"); }); it("should display error when TwainService fails", fakeAsync(() => { // observable error after delay const q$ = cold("---#|", null, new Error("TwainService test failure")); getQuoteSpy.and.returnValue(q$); fixture.detectChanges(); // ngOnInit() expect(quoteEl.textContent).toBe("...", "should show placeholder"); getTestScheduler().flush(); // flush the observables tick(); // component shows error after a setTimeout() fixture.detectChanges(); // update error message expect(component.errorMessage).toMatch( /test failure/, "should display error" ); expect(quoteEl.textContent).toBe("...", "should show placeholder"); })); });
The beauty of marble testing is in the visual definition of the observable streams. This test defines a cold observable that waits three frames (---
), emits a value (x
), and completes (|
). In the second argument you map the value marker (x
) to the emitted value (testQuote
).
const q$ = cold("-a|", { a: testQuote });
For error case:
const q$ = cold('---#|', null, new Error('TwainService test failure'));
This is a cold observable that waits three frames and then emits an error, The hash (#
) indicates the timing of the error that is specified in the third argument. The second argument is null because the observable never emits a value.