Table of Contents
URL: https://www.progressiverobot.com/rxjs-marble-testing/
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 github.com, 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.
[label color.enum.ts]
export enum Color {
NONE,
RED,
ORANGE,
YELLOW,
GREEN,
BLUE,
PURPLE,
BLACK
}
[label 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:
[label 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
}
}
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) === 😍<^>