I recently came across a nice way of testing observables called “marble testing”. It’s called marble testing because marble diagrams, the diagrams in the documentation of the library, are used to assert behavior and values, and create mock observables for the observable under test.
Marble diagrams are usually pictures, but in our tests, a marble diagram is simply a string which looks something like this:
const input$ = "--a----b--c|";
It represents events occurring over “virtual time”.
-: Represents a frame, and it is equal to 1ms of virtual time for our observable. It is possible to configure the amount of virtual time.
[a-z0-9]: Represents a value being emitted by the observable, and advances time by one frame.
(abc): Groups multiple values that are expected to be emitted in a single frame. It also advances virtual time by number values emitted plus 2 for
[0-9](ms|s|m): Represents amount of virtual time, you can use it as a replacement of -.
|: Represents a complete signal i.e., observable has completed and has nothing more to emit.
#: Represents an error being thrown from the observable.
^(only in ‘hot’ observables): Represents the point in time where a subscription is expected, and represent the 0 frame, so
--^--a--b--| shows that a subscription is expected at
^. The frames before
^ are -ve, and the ones after it are +ve.
!: Represent unsubscription point.
! can be used to assert when was an observable was subscribed and unsubscribed to, and also to guide when should the observable being tested be subscribed and unsubscribed to. I’ve added a few example that’ll make it more clear.
Before we begin writing tests it is important that we understand the difference between hot and cold observables. There are a few ways to describe hot and cold observables, so I’d suggest reading up on it a bit here.
The easiest explanation is that in a hot observable, the producer is not part of the observable and it emits values whether it has any subscribers or not, for example an observable over mouse move event.
A cold observable emits values only when it is subscribed to; the producer is created when the observable is subscribed to, for example an
ajax request with ajax operator.
Let’s test an observable that emits two values with an interval of 10ms, increments them by 1 and then completes afterwards.
rxjs/testing, instantiate it with the function to perform the assertion. I’m using TestScheduler for simplicity but you can also use
jest-marbles for writing marble tests.
Let’s finally write our test. We can represent
input$ behavior in marble diagram as
10ms a 9ms (b|). Why is there a 9ms if the values are emitted after 10ms? because, just like
-, a symbol representing a value also advances the frame by
1ms of virtual time, so when
a will be emitted, 11ms of virtual time will have passed and because of that, second value
b will be emitted 9ms after
a and the observable will complete on that frame which is why the complete signal is grouped with
We passed a function to
scheduler.run() which will be called with some helpers to mock hot and cold observables for the observable under test, queue assertions etc. One these helpers is
expectObservable, we’ll use it to queue our assertion. Assertions are executed synchronously after our callback is executed. We can also run assertions while our callback is being executed by calling
scheduler.run() does that for us anyway.
Let’s write another test for an observable that subscribes over an observable of input events.
our test will look something like this:
One more thing you can control, is when the TestScheduler subscribes to and unsubscribes from the observable under test. expectObservable helper takes a second string argument called “subscription marble” that does that.
subMarble, TestScheduler is being instructed to subscribe to
output$ a frame before
input$ emits any value and unsubscribe from
output$ two frames after it emits its first value. Because of early unsubscription TestScheduler only receives one value i.e.,
a, which is why we had to update
outputMarbles and values.
Hopefully this post has given you enough understanding to start writing your tests and jump into documentation.