Writing Marble Tests

This document refers to the writing marble tests for the RxJS repo internals and is intended for anyone wishing to help maintain the RxJS repo. Users of RxJS should instead view the guide for writing marbles tests for applications. The major difference is that the behavior of the TestScheduler differs between manual usage and using the `testScheduler.run(callback)` helper.

"Marble Tests" are tests that use a specialized VirtualScheduler called the TestScheduler. They enable us to test asynchronous operations in a synchronous and dependable manner. The "marble notation" is something that's been adapted from many teachings and documents by people such as @jhusain, @headinthebox, @mattpodwysocki and @andrestaltz. In fact, André Staltz first recommended this as a DSL for creating unit tests, and it has since been altered and adopted.

See also

Basic methods

The unit tests have helper methods that have been added to make creating tests easier.

Ergonomic defaults for hot and cold

In both hot and cold methods, value characters specified in marble diagrams are emitted as strings unless a values argument is passed to the method. Therefor:

hot('--a--b') will emit "a" and "b" whereas

hot('--a--b', { a: 1, b: 2 }) will emit 1 and 2.

Likewise, unspecified errors will just default to the string "error", so:

hot('---#') will emit error "error" whereas

hot('---#', null, new SpecialError('test')) will emit new SpecialError('test')

Marble Syntax

Marble syntax is a string which represents events happening over "time". The first character of any marble string

always represents the "zero frame". A "frame" is somewhat analogous to a virtual millisecond.

Examples

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

|: Equivalent to Observable.empty()

#: Equivalent to Observable.throw()

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

'--a--b--|': On frame 20 emit a, on frame 50 emit b, and on frame 80, complete

'--a--b--#': On frame 20 emit a, on frame 50 emit b, and on frame 80, error

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

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

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

Subscription Marble Syntax

The subscription marble syntax is slightly different to conventional marble syntax. It represents the subscription and an unsubscription points happening over time. There should be no other type of event represented in such diagram.

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 20 "frames" of time passed, and the subscription was not unsubscribed.

'--^--!-': on frame 20 a subscription happened, and on frame 50 was unsubscribed.

Anatomy of a Test

A basic test might look as follows:

const e1 = hot('----a--^--b-------c--|'); const e2 = hot( '---d-^--e---------f-----|'); const expected = '---(be)----c-f-----|'; expectObservable(e1.merge(e2)).toBe(expected);

A test example with specified values:

const values = { a: 1, b: 2, c: 3, d: 4, x: 1 + 3, // a + c y: 2 + 4, // b + d } const e1 = hot('---a---b---|', values); const e2 = hot('-----c---d---|', values); const expected = '-----x---y---|'; expectObservable(e1.zip(e2, function(x, y) { return x + y; })) .toBe(expected, values);

A test example with subscription assertions:

const x = cold( '--a---b---c--|'); const xsubs = '------^-------!'; const y = cold( '---d--e---f---|'); const ysubs = '--------------^-------------!'; const e1 = hot( '------x-------y------|', { x: x, y: y }); const expected = '--------a---b----d--e---f---|'; expectObservable(e1.switch()).toBe(expected); expectSubscriptions(x.subscriptions).toBe(xsubs); expectSubscriptions(y.subscriptions).toBe(ysubs);

In most tests it will be unnecessary to test subscription and unsubscription points, being either obvious or implied from the expected diagram. In those cases do not write subscription assertions. In test cases that have inner subscriptions or cold observables with multiple subscribers, these subscription assertions can be useful.

Generating PNG marble diagrams from tests

Typically, each test case in Jasmine is written as it('should do something', function () { /* ... */ }). To mark a test case for PNG diagram generation, you must use the asDiagram(label) function, like this:

it.asDiagram(operatorLabel)('should do something', function () { });

For instance, with zip, we would write

it.asDiagram('zip')('should zip by concatenating', function () { const e1 = hot('---a---b---|'); const e2 = hot('-----c---d---|'); const expected = '-----x---y---|'; const values = { x: 'ac', y: 'bd' }; const result = e1.zip(e2, function(x, y) { return String(x) + String(y); }); expectObservable(result).toBe(expected, values); });

Then, when running npm run tests2png, this test case will be parsed and a PNG file zip.png (filename determined by ${operatorLabel}.png) will be created in the img/ folder.