Testing RxJS Code with Marble Diagrams

This guide refers to usage of marble diagrams when using the new `testScheduler.run(callback)`. Some details here do not apply to using the TestScheduler manually, without using the `run()` helper.

We can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler. ASCII marble diagrams provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.

At this time the TestScheduler can only be used to test code that uses timers, like delay/debounceTime/etc (i.e. it uses AsyncScheduler with delays > 1). If the code consumes a Promise or does scheduling with AsapScheduler/AnimationFrameScheduler/etc it cannot be reliably tested with TestScheduler, but instead should be tested more traditionally. See the Known Issues section for more details.

import { TestScheduler } from 'rxjs/testing'; const scheduler = new TestScheduler> ((actual, expected) => { // asserting the two objects are equal // e.g. using chai. expect(actual).deep.equal(expected); }); // This test will actually run *synchronously* it('generate the stream correctly', () => { scheduler.run(helpers => { const { cold, expectObservable, expectSubscriptions } = helpers; const e1 = cold('-a--b--c---|'); const subs = '^----------!'; const expected = '-a-----c---|'; expectObservable(e1.pipe(throttleTime(3, scheduler))).toBe(expected); expectSubscriptions(e1.subscriptions).toBe(subs); }); });

API

The callback function you provide to testScheduler.run(callback) is called with helpers object that contains functions you'll use to write your tests.

When the code inside this callback is being executed, any operator that uses timers/AsyncScheduler (like delay, debounceTime, etc) will **automatically** use the TestScheduler instead, so that we have "virtual time". You do not need to pass the TestScheduler to them, like in the past.
testScheduler.run(helpers => { const { cold, hot, expectObservable, expectSubscriptions, flush } = helpers; // use them });

Marble Syntax

In the context of TestScheduler, a marble diagram is a string containing special syntax representing events happening over virtual time. Time progresses by frames. The first character of any marble string always represents the zero frame, or the start of time. Inside of testScheduler.run(callback) the frameTimeFactor is set to 1, which means one frame is equal to one virtual millisecond.

How many virtual milliseconds one frame represents depends on the value of TestScheduler.frameTimeFactor. For legacy reasons the value of frameTimeFactor is 1 only when your code inside the testScheduler.run(callback) callback is running. Outside of it, it's set to 10. This will likely change in a future version of RxJS so that it is always 1.

IMPORTANT: This syntax guide refers to usage of marble diagrams when using the new testScheduler.run(callback). The semantics of marble diagrams when using the TestScheduler manually are different, and some features like the new time progression syntax are not supported.

Time progression syntax

The new time progression syntax takes inspiration from the CSS duration syntax. It's a number (int or float) immediately followed by a unit; ms (milliseconds), s (seconds), m (minutes). e.g. 100ms, 1.4s, 5.25m.

When it's not the first character of the diagram it must be padded a space before/after to disambiguate it from a series of marbles. e.g. a 1ms b needs the spaces because a1msb will be interpreted as ['a', '1', 'm', 's', 'b'] where each of these characters is a value that will be next()'d as-is.

NOTE: You may have to subtract 1 millisecond from the time you want to progress because the alphanumeric marbles (representing an actual emitted value) advance time 1 virtual frame themselves already, after they emit. This can be very unintuitive and frustrating, but for now it is indeed correct.

const input = ' -a-b-c|'; const expected = '-- 9ms a 9ms b 9ms (c|)'; /* // Depending on your personal preferences you could also // use frame dashes to keep vertical aligment with the input const input = ' -a-b-c|'; const expected = '------- 4ms a 9ms b 9ms (c|)'; // or const expected = '-----------a 9ms b 9ms (c|)'; */ const result = cold(input).pipe( concatMap(d => of(d).pipe( delay(10) )) ); expectObservable(result).toBe(expected);

Examples

'-' or '------': Equivalent to never(), or an observable that never emits or completes

|: Equivalent to empty()

#: Equivalent to throwError()

'--a--': An observable that waits 2 "frames", emits value a and then never completes.

'--a--b--|': On frame 2 emit a, on frame 5 emit b, and on frame 8, complete

'--a--b--#': On frame 2 emit a, on frame 5 emit b, and on frame 8, error

'-a-^-b--|': In a hot observable, on frame -2 emit a, then on frame 2 emit b, and on frame 5, complete.

'--(abc)-|': on frame 2 emit a, b, and c, then on frame 8 complete

'-----(a|)': on frame 5 emit a and complete.

'a 9ms b 9s c': on frame 0 emit a, on frame 10 emit b, on frame 10,012 emit c, then on on frame 10,013 complete.

'--a 2.5m b': on frame 2 emit a, on frame 150,003 emit b and never complete.

Subscription Marbles

The expectSubscriptions helper allows you to assert that a cold() or hot() Observable you created was subscribed/unsubscribed to at the correct point in time. The subscription marble syntax is slightly different to conventional marble syntax.

There should be at most one ^ point in a subscription marble diagram, and at most one ! point. Other than that, the - character is the only one allowed in a subscription marble diagram.

Examples

'-' or '------': no subscription ever happened.

'--^--': a subscription happened after 2 "frames" of time passed, and the subscription was not unsubscribed.

'--^--!-': on frame 2 a subscription happened, and on frame 5 was unsubscribed.

'500ms ^ 1s !': on frame 500 a subscription happened, and on frame 1,501 was unsubscribed.


Known Issues

You can't directly test RxJS code that consumes Promises or uses any of the other schedulers (e.g. AsapScheduler)

If you have RxJS code that uses any other form of async scheduling other than AsyncScheduler, e.g. Promises, AsapScheduler, etc. you can't reliably use marble diagrams for that particular code. This is because those other scheduling methods won't be virtualized or known to TestScheduler.

The solution is to test that code in isolation, with the traditional async testing methods of your testing framework. The specifics depend on your testing framework of choice, but here's a pseudo-code example:

// Some RxJS code that also consumes a Promise, so TestScheduler won't be able // to correctly virtualize and the test will always be really async const myAsyncCode = () => from(Promise.resolve('something')); it('has async code', done => { myAsyncCode().subscribe(d => { assertEqual(d, 'something'); done(); }); });

On a related note, you also can't currently assert delays of zero, even with AsyncScheduler, e.g. delay(0) is like saying setTimeout(work, 0). This schedules a new "task" aka "macrotask", so it's async, but without an explicit passage of time.

Behavior is different outside of testScheduler.run(callback)

The TestScheduler has been around since v5, but was actually intended for testing RxJS itself by the maintainers, rather than for use in regular user apps. Because of this, some of the default behaviors and features of the TestScheduler didn't work well (or at all) for users. In v6 we introduced the testScheduler.run(callback) method which allowed us to provide new defaults and features in a non-breaking way, but it's still possible to use the TestScheduler outside of testScheduler.run(callback). It's important to note that if you do so, there are some major differences in how it will behave.

While at this time usage of the TestScheduler outside of testScheduler.run(callback) has not been officially deprecated, it is discouraged because it is likely to cause confusion.