Tutorial

Marble Testing RxJS Observables

Published on August 12, 2017
    Default avatar

    By Mark P. Kennedy

    Marble Testing RxJS Observables

    While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

    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).

    debounceTime

    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
      }
    }
    
    

    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) === 😍

    Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

    Learn more about us


    About the authors
    Default avatar
    Mark P. Kennedy

    author

    Still looking for an answer?

    Ask a questionSearch for more help

    Was this helpful?
     
    Leave a comment
    

    This textbox defaults to using Markdown to format your answer.

    You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

    Try DigitalOcean for free

    Click below to sign up and get $200 of credit to try our products over 60 days!

    Sign up

    Join the Tech Talk
    Success! Thank you! Please check your email for further details.

    Please complete your information!

    Get our biweekly newsletter

    Sign up for Infrastructure as a Newsletter.

    Hollie's Hub for Good

    Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

    Become a contributor

    Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

    Welcome to the developer cloud

    DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

    Learn more
    DigitalOcean Cloud Control Panel