Sunday, 13 August, 2017 UTC


Summary

RxJS Observables are a really powerful and elegant way to compose asynchronous code but can get complex to test. Testing is made much easier with marble testing.
This post explains marble testing and an example of how we would use it to test a ColorMixer. The ColorMixer example and tests are written in Typescript, but RxJS and marble testing can be used with vanilla Javascript as well.
This post assumes basic knowledge of RxJS Observables and operators.
Marble diagrams
Marble diagrams are a way to visually represent Observables. The marbles represent a value being emitted, the passage of time is represented from left to right, a vertical line represents the completion of an Observable, and an X represents an error.
With just these basic pieces, any Observable can be represented. They are most commonly used to show how an operator transforms an observable. Here is an example from the RxJS docs for the debounceTime operator (used in ColorMixer).

Image courtesy of reactivex.io. You can read more about marble diagrams here.

Marble testing is using marble diagrams in your tests. There are multiple libraries for marble testing but we will use jasmine-marbles in the example because we will be testing the ColorMixer with jasmine but rxjs-marbles is another great implementation that is test framework agnostic.
Everything you need to know about marble testing can be found here, but the basics are as follows:
  • The RxJS TestScheduler controls the passage of time and when values are emitted from Observables created in the tests.
  • Observables are created with the cold(marbles, values?, errors?) (subscription starts when the test begins) or hot(marbles, values?, errors?) (already “running” when the test begins) methods.
  • - represents the passage of 10 frames of time.
  • | represents the completion of an Observable.
  • ^ represents the subscription point of an Observable (only valid for hot Observables).
  • # represents an error. The value of the error can be provided to errors argument.
  • any other character represents a value emitted. The actual value can be represented in the values argument, where the character is the key.
  • Finally, Observables can be compared with the expectObservable method.
To test our ColorMixer we first need to install a marble testing library:
npm install jasmine-marbles --save-dev
Testing ColorMixer
The ColorMixer has one static method, mix, that takes Observables of whether or not a color is going into the mixer. When executed, this method will return an Observable of what color the mixer is outputting. It will also mix the colors together for a certain period of time to insure that the color coming out is mixed well.
color.enum.ts
export enum Color { NONE, RED, ORANGE, YELLOW, GREEN, BLUE, PURPLE, BLACK }
color-mixer.ts
import 'rxjs/add/observable/combineLatest'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; import 'rxjs/add/operator/startWith'; import { Observable } from 'rxjs/Observable'; import { IScheduler } from 'rxjs/Scheduler'; import { async } from 'rxjs/scheduler/async'; import { Color } from './color.enum'; export class ColorMixer { static mix(r: Observable<boolean>, y: Observable<boolean>, b: Observable<boolean>, // Allow configuration during testing mixingTime = 1000, // Allow the use of the TestScheduler during testing scheduler: IScheduler = async): Observable<Color> { return Observable.combineLatest( // Every color starts off r.startWith(false), y.startWith(false), b.startWith(false), // Mix the colors (redOn, yellowOn, blueOn) => { if (!redOn && !yellowOn && !blueOn) { return Color.NONE; } else if (redOn && !yellowOn && !blueOn) { return Color.RED; } else if (redOn && yellowOn && !blueOn) { return Color.ORANGE; } else if (!redOn && yellowOn && !blueOn) { return Color.YELLOW; } else if (!redOn && yellowOn && blueOn) { return Color.GREEN; } else if (!redOn && !yellowOn && blueOn) { return Color.BLUE; } else if (redOn && !yellowOn && blueOn) { return Color.PURPLE; } else { return Color.BLACK; } }) .debounceTime(mixingTime, scheduler) .startWith(Color.NONE) .distinctUntilChanged(); } }

ColorMixer uses a Scheduler because of the debounceTime operator. In order to marble test it properly we need to tell it to use the TestScheduler to allow it to act as a virtual clock. We also need to modify the mixingTime to be the number of frames we want instead of milliseconds. The ColorMixer can now be properly tested:
color-mixer.spec.ts
import { ColorMixer } from './color-mixer'; import { cold, getTestScheduler } from 'jasmine-marbles'; import { Color } from './color.enum'; import { Observable } from 'rxjs/Observable'; describe('ColorMixer', () => { describe('mix', () => { it('should mix colors', () => { const r = cold('--o--x--|', onOffMarbles()); const y = cold('--------|', onOffMarbles()); const b = cold('--o-----|', onOffMarbles());  // Start mixing red and blue @ frame 20. // Purple is made @ frame 40 (20 frame mixing time). // Remove red @ frame 50 to make blue @ frame 70. const c = cold('x---p--b|', colorMarbles()); expect(mix(r, y, b)).toBeObservable(c); }); }); }); // Change the mixing time to 20 frames and use the TestScheduler function mix(r: Observable<boolean>, y: Observable<boolean>, b: Observable<boolean>) { return ColorMixer.mix(r, y, b, 20, getTestScheduler()); } // Marble values representing on/off function onOffMarbles() { return { o: true, x: false } } // Marble values representing colors function colorMarbles() { return { x: Color.NONE, r: Color.RED, o: Color.ORANGE, y: Color.YELLOW, g: Color.GREEN, b: Color.BLUE, p: Color.PURPLE, B: Color.BLACK } }
When tests pass, expect(…).toBeObservable(…) acts just like any other assertion. When the assertion fails, a detailed log is output describing what happened in each frame of the Observable. If we forgot to add the Color.BLUE marble at the end of our expected Observable we would get:
Expected {"frame":0,"notification":{"kind":"N","value":0,"hasValue":true}} {"frame":40,"notification":{"kind":"N","value":6,"hasValue":true}} {"frame":70,"notification":{"kind":"N","value":5,"hasValue":true}} {"frame":80,"notification":{"kind":"C","hasValue":false}} to deep equal {"frame":0,"notification":{"kind":"N","value":0,"hasValue":true}} {"frame":40,"notification":{"kind":"N","value":6,"hasValue":true}} {"frame":80,"notification":{"kind":"C","hasValue":false}}
The values correspond to the Color enum values. It is clear that Color.BLUE was emitted at frame 70 that we forgot to add to our assertion.

Marble testing allows for a visual 👀 way to test Observables. It makes them easier to test and read.
observable$ + (jasmine-marbles || rxjs-marbles) === 😍