The ultimate guide — never again be confused on how to test Observables
Reactive Programming is the new way of handling asynchronous code. And it is super powerful!
If you came across this blog post, I assume that you already wrote some lines of reactive code with RxJS if not a thousand. So I don’t have to tell you how it works and how cool it is. 😎
I guess we are all on the same page — writing productive code with RxJS is powerful and makes a lot of fun. But what about testing?
What we’re going to learn 👨🎓
In this blog post, we are going to see the two different strategies on how to test an Observable.
The “subscribe and assert pattern” and “marble testing”.
We will apply those strategies in different scenarios to see their advantages and downsides. The comparison of those strategies in different situations leads us to a clear picture of how to test Observables.
Let’s start with the “subscribe and assert” pattern.
The Subscribe and assert pattern
You may already know that Observables are lazy! They don’t do anything until we subscribe to them. In a nutshell, no subscription — no values.
Same counts if we use Observables in our tests. An Observable never emits a value in a test if we don’t subscribe to it. To solve this problem, developers often tend to subscribe to our Observable to kick it off.
This is what we call the “subscribe and assert” pattern.
Nothing fancy. We create ourself a testeee and subscribe to to the result$ stream to kick off the emission of values.
From working with different project teams and consulting developers, I feel that this is the approach most developers use to test their Observables. While this is a valid and legit approach, it still has some downsides.
Downsides of the subscribe and assert pattern
There are cases where the “subscribe and assert” pattern has some downsides and where it may not be the best pattern to follow. Let’s have a look at some disadvantages of this approach.
1. Don’t forget to call “done” when you’re done ☝️
Often, tests which use the “subscribe and assert pattern” are green even though, in reality, they are failing. How come? 🤔
In asynchronous scenarios, our test rushes through without also checking our assertions inside our next, complete or error handler. This can quickly happen if we forget to call the done callback after the assertions.
The done callback is a way to indicate to the testing framework when our test ist actually done.
So always remember:
If we forget to call the done Callback in an asynchronous scenario, the test might finish and be green without actually checking our assertion.
2. Asserting multiple values
Let’s say we have a simple stream that emits the ingredients of a Pizza.
const pizzaIngredients$ = of(🍕, 🍅, 🧀, 🌶️, 🍄);
Now imagine we want to test this with the “subscribe and assert pattern”. Easy? Isn’t it?
Well, we can’t just compare the values with a resulting array as we would in regular tests. We need to remember that the values are emitted over time. In our case, they come immediately but still value after value.
A lot of developers tend to use a counter to compare the emitted value with an expected value from an array.
Since this is a completing stream, we could also take another approach and use the toArray operator to put the values into an array and then compare them.
We have two different ways to test a simple Observable. 👍
But the thing I want to point out in those two examples is that even such a simple stream requires us to implement some logic in the form of a counter or forces us to use an operator like toArray.
This boils our test, decreases readability, and adds some unwanted testing logic.
3. Unable to assert that values are emitted at the correct time
The pizza code from above is straightforward. It immediately emits all ingredients. Let’s take this program one step further and make it more sophisticated.
What if our program emits each ingredient at the time it should be added to the Pizza. Let’s visualize this in the form of a Cookbook. 📘
The cookbook contains a collection of ingredients. Each ingredient has a name and a time when it should be added to the Pizza.
Let’s sprinkle some RxJS magic around the cookbook to make it emit the values at the correct time. 🧙
We delay each emission with the specified value from the cookbook using the delayWhen operator. Finally we pluck the ingredients name because that’s the value we are interested in.
Okay — we extended our cooking application. But how to test that the values are emitted at the correct time with the “subscribe and assert” pattern? 😮
It gets trickier, right?
If we test this with a standard approach, our test will take 3 seconds at least. If the last item would be emitted after 20 seconds instead of 3? Our test would fail. So that’s no solution.
We don’t want our unit test to take up 3 seconds; we want it to be fast and reliable. Therefore we need to have some virtual time. Currently, I don’t know a consistent cross framework compatible way to test timed Observables with the “subscribe and assert” pattern. But bear with me, there’s a way, and we will get there.
The disadvantages of the “subscribe and assert” pattern can be summarized in the following way.
- In an asynchronous test, we always need to call the done callback — otherwise, our test causes wrong results.
- In most tests, we end up with boiled tests and unnecessary testing logic
- We are unable to test timed Observables
But hang tight, there is a way to solve those problems. And the answer is Marble testing with RxJs’s testing utilities.
Marble testing — the intuitive and clean way to test Observables
Once you start your path to master Observables, the chances are high that you already encountered a marble diagram on your way.
A a Marble diagram is a visual representation of an Observable. It is an RxJS domain-specific language that helps us describe what happens with our Observable.
The marble syntax is a very intuitive syntax to represent streams. It is often used to visualize observable transformations.
The diagram illustrates a source Observable that emits three values and then after some time passed, completes.
We apply the map operator on this Observable to end up with a resulting Observable that emits the mapped values (the original values multiplied by ten) which are 10, 20 and 30. As the source, also our resulting observable completes.
Marble diagrams are a perfect tool to visualize Observable chains. They are useful to discuss, understand, or to teach streams. In a nutshell, they are a form of documentation which is simple to understand and delightful to read. So why not also use them in code?
Marble testing syntax
We learned that marble diagrams are fresh and pleasant to read, but how do we use them in our test code? We don’t have these lovely bubbles, right?
Instead of bubbles, RxJs introduced the following syntax when writing marble tests in our code.
- ' ' the whitespace is a unique character that will not be interpreted; it can be used to align your marble string.
- '-' represents a frame of virtual time passing
- '|' This sign illustrates the completion of an observable.
- '#' Signifies an error
- [a-z] an alphanumeric character represents a value which is emitted by the Observable.
- '()' used to group events in the same frame. This can be used to group values, errors, and completion.
- '^' this sign illustrates the subscription point and will only be used when we are dealing with hot observables.
That’s the basic syntax. Let’s look at some examples to make ourself more familiar with the syntax.
- —--: equivalent to NEVER. An observable that never emits
- -a--b--c| : an Observable that emits a on the first frame, b on the fourth and c on the seventh. After emitting c the observable completes.
- --ab--# : An Observable that emits a on frame two, b on frame three and an error on frame six.
- -a^(bc)--|: A hot Observable that emits a before the subscription.
You get the idea, right. But how to get started?
Applying marble tests in our code
So we have seen the marble syntax — let’s have a look at how we can actually “marble test” our code.
There are different libraries out there that allow you to write marble tests. The most known are probably “jasmine-marbles”, “jest-marbles” and “rxjs-marbles”.
But RxJS itself also provides testing utils. All those libraries are just wrappers around the testing utils. I prefer to work directly with the testing tools of RxJS. Why?
- It’s a dependency less.
- You are always up to date with the core implementation.
- You are up to date with the latest features.
So in the following examples, we will work with RxJs TestScheduler.
☝ ️If you want to know more about the RxJS testing utils or how to set up a test with the RxJS TestScheduler I recommend you to check out my blog post about “Marble testing with RxJS testing utils”
Okay, let’s revisit the Pizza example.
Pizza with marbles 🍕
Rember the Pizza code from above?
const pizzaIngredients$ = of(🍕, 🍅, 🧀, 🌶️, 🍄);
In the “subscribe and assert” pattern, we had to write a lot of assertion logic to test that the ingredients are emitted in the correct order.
Things get much more comfortable when we use marble testing. We can write the following marble test.
As explained in my other blogpost about “Marble testing with RxJS testing utils” we create ourselves a fresh instance of the TestScheduler. We then use its run method to test our streams.
We destructure the expectObservable method from the RunHelpers and use it to compare our pizzaIngredient$ to meet our expectedMarble with the expectedIngredients.
Nice, this is already much cleaner! But this is the easy example. What about the more sophisticated pizza code?
Time progression syntax
Remember the pizza example that emitted ingredients over time?
The RunHelpers aren’t the only thing that is super cool about the TestScheduler. The beautiful thing is the “Virtualized time”.
What does that mean? It means that we can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler.
Everything that runs inside the run callback automatically uses the TestScheduler! How cool is that!
☝️If you’re not familiar with Virtualized time, time progression syntax and the run callback I recommend you to check out my blogpost about “Testing asynchronous RxJs operators”
With the use of the knowledge described in the “Testing asynchronous RxJS operators” blog post, we can create ourselves a test that not only asserts that the correct items are emitted but also asserts that they are emitted at the correct time.
The most important thing about this test is the expectedMarble diagram.
a 999ms (bc) 996ms d 999ms (e|)’;
This diagram indicates that a is emitted immediately. Then after one second b and c is emitted. Then again, one second later d is emitted. Another second subsequently e emits, and the stream finally completes.
Why 999ms or 996ms? If those numbers look strange to you I again recommend you to read my blogpost about “Testing asynchronous RxJs operators” you will find the answer there 😉
If not, well then you either allready knew the pitfalls of the time progression syntax or you read my blog post (thanks for that 😉)
Comparing the approaches
We tested the same two Pizza scenarios with the “subscribe and assert” pattern as well as with “marble testing”.
Let’s compare them again.
1. Synchronous Pizza
First, we had a look at the synchronous Pizza example.
const pizzaIngredients$ = of(🍕, 🍅, 🧀, 🌶️, 🍄);
We tested this example with the “subscribe and assert” pattern as well as with the “marble testing pattern”.
If we look at those two approaches, we immediately notice that the “subscribe and assert” pattern contains some testing logic in the form of a counter. The marble testing example, on the other hand, is more declarative and in my opinion, much cleaner and less error-prone.
2. Asynchronous Pizza
The previous case was straightforward. We extended the example with a Cookbook so that the Pizza ingredients get delivered asynchronously.
As you can see, there’s not much to compare as this scenario is only testable with marble testing. Maybe it is somehow with the subscribe and assert pattern, but I did not figure out a clean framework agnostic way. Perhaps anyone in the community?
How to test Observables — my advice
There are two different strategies when it comes to testing Observables.
- The subscribe and assert pattern
- The marble testing pattern
The “subscribe and assert pattern” has the following downsides
- You need to remind yourself always to call the done callback
- Asserting multiple values often requires unnecessary testing logic
- No way to test asynchronous code
Marble testing, on the other side, offers a nice, clean, and declarative way to test your Observables. I think we should always try to test your Observables with the marble approach.
Marble testing itself is best done by directly using the RxJs TestScheduler with its run callback.
The run Callback executes every operator that uses the AsyncScheduler by default in virtualized time. We can then use the time progression syntax as an API to control virtualized time.
🧞 🙏 If you liked this post, share it and give some claps👏🏻 by clicking multiple times on the clap button on the left side.
Claps help other people to discover content and motivate me to write more 😉
Feel free to check out some of my other articles about front-end development.