A few days ago I faced a problem testing my observable that was dependent on RxJS Subject in the Angular application.
The main problem is with the subscriptions. It is impossible to properly manage subscriptions to the Observable and emit Subject effectively using the built-in RxJS testing helpers.
The RxJS community is large, but still, I haven’t found an answer to the problem on the Internet.
For this reason, I decided to prepare an article that would help you organize testing of your observables that are dependent on subjects.
Feel free to open CodeSandbox at any time during the reading if you want to interact with the solution.
In order to understand the necessity of the information presented in the article, ideally you needed to face such a problem in the past and look up answers somewhere.
If you already faced it, welcome to the club, the article might help you get yourself out of a dead end.
Otherwise, If you want to broaden your knowledge of RxJS and grasp a new concept while binge-reading on a weekend, some experience using Javascript, RxJS, and Jest would be nice to have to understand how the programs in the code snippets work.
Finally, anyone who wants to continue reading this article is highly welcomed. The article uses as less dependencies as possible and as simple code as I could have come up with.
Grab a cup of coffee and be prepared to tackle the nasty problem together with me today.
Let’s put some code below in order to present an example of the observable to be tested.
The example is totally made up in order to simplify your understanding of the code and to reduce the usage of other dependencies.
The use case of the observable is from the real-world application, but the names are changed in order to generalize the domain.
import { of, Subject } from 'rxjs';
import { filter, mapTo, switchMap, tap } from 'rxjs/operators';
const actions$ = of('do', 'work', 'brew', 'coffee');
const coffee$ = (new Subject()).pipe(mapTo('your coffee is ready ☕️'));
export const reaction$ = actions$.pipe(
filter((action) => action === 'coffee'),
switchMap(() => coffee$),
tap((coffeeReadyMessage) => document.write(coffeeReadyMessage)),
mapTo('yum')
)
reaction$.subscribe();
window.setTimeout(() => {
coffee$.next();
}, 5000)
A question arises now, how can we test the reaction$ observable properly?
An intuitive first attempt would be just putting some code in your editor so let’s try it.
import { TestScheduler } from 'rxjs/testing';
import { reaction$ } from './index';
describe('reaction$', () => {
it('should wait for coffee and emit "yum"', () => {
const testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
})
testScheduler.run(({ expectObservable }) => {
// coffee$.next() - to early, we haven't subscribed yet
// whoops, how can I emit values from my coffee subject now?
expectObservable(reaction$).toBe('a', { a: 'yum' })
// cofeee$.next() - to late, assertion done above
})
})
})
Let me comment on the problem presented above.
expectObservable is a helper method that is presented to us by the TestScheduler from rxjs/testing module.
We use it to subscribe to the reaction$ observable and use marble diagram assertion for the sake of testing simplicity.
As you might have remembered, the reaction$ observable awaits the coffee$ subject in order to emit its own value.
From the code snippet above we see that it is hard to come up with a correct spot to emit values from the coffee$ subject.
If we emit the coffee$ values before the subscription to the observable, nobody listens and as a result, the reaction$ observable never emits.
Alternatively, if we emit the coffee$ values after the subscription to the observable, it is too late and the assertion already took place.
The question appears. How can such an observable be tested using RxJS?
Creating a composite observable that can be tested instead of the reaction$ observable can help us in such a case.
I know, it is hard to understand what I mean, but why use even more words if we have such a great tool as code snippets at our service?
Take a look at the code, I am sure it will express the idea a hundred times better than words do.
import { merge, of } from "rxjs";
import { delay, ignoreElements, tap } from "rxjs/operators";
import { TestScheduler } from "rxjs/testing";
import { reaction$, coffee$ } from "./index";
describe("reaction$", () => {
it('should wait for coffee and emit "yum"', () => {
const testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
const COFFEE_WAITING_TIME = 500;
const composite$ = merge(
reaction$,
of(null).pipe(
delay(COFFEE_WAITING_TIME),
tap(() => coffee$.next()),
ignoreElements()
)
);
testScheduler.run(({ expectObservable }) => {
expectObservable(composite$).toBe(`${COFFEE_WAITING_TIME}ms a`, { a: "yum" });
});
});
});
The tests are green now which makes us happy and allows us to deliver the functionality to our end users.
To sum up the approach, we create a composite$ observable that encapsulates both the subscription to the reaction$ observable and emitting the coffee$ subject upon subscription to the composite$ observable.
A small tip, make sure to include the ignoreElements operators to your coffee$ subject emitter or else the assertion will fail due to null values that should be accounted for in the marble diagram.
A complete interactive solution below
Use a composite observable that combines the subscription to the observable and emits the subject of interest.
Test the composite observable instead of the observable of interest.
This approach makes it possible to test the RxJS Observable with dependency on Subject in a simple manner using marble diagrams.
Except for this approach, there is a manual approach of testing is available where you manage subscriptions on your own and test your solution without marble diagrams, but it’s less elegant and straightforward.
I hope you liked the reading, check my other articles and contact me to suggest other topics for my next articles.
Thank you for reading