How to marble test RxJS Subjects

How to bridge the gap between the timing of function calls and Observables

Image for post
Image for post

Asynchronous code is hard! It’s hard to write and to test.

RxJs has changed the way we think about asynchrony. Instead of using Promises, we nowadays deal with streams in the form of Observables or Subjects. RxJs provides us with many out of the box operators to create, merge, or transform streams.

But we do not only get great tools for runtime code, but we also get amazing tools to test streams.

Marble diagrams are a great way of modeling streams. They are used throughout many tutorials and can now also be used in tests to assert streams.

In case you never heard of marble testing I recommend you to checkout the following article.

Marble testing is great! Many people use it to test their Observables. But what about Subjects?

A pizza service 🍕

Nowadays, most frameworks use the concept of components. When working with components, you often need a way of communication between them. Usually, you have multiple ways of implementing communication between components.

Angular, for example, offers component communication over @Input and @Output, communication over a service or, for a more sophisticated use case, the use of a state management library such as ngrx.

For simple use cases, a service with a Subject is often a nice solution. Imagine an app where you can order Pizzas.

Image for post
Image for post
A simple pizza app that uses a Service with a Subject to communicate between components.

A pizza can be ordered by clicking a button on a list item. The button click then uses the OrderService to inform the shopping cart that a new pizza has been added. Let’s have a look at the code of the OrderService.

Image for post
Image for post

Our OrderService has a public orderPizza method that will next a new Pizza into our pizza$.

☝ ️You should never expose a Subject directly. It’s always better to only expose an Observable. To do so you can always use the asObservable method of a Subject. To keep things as simple as possible and focus on the testing Problem we leave that away for now.

Okay cool. We implemented an impeccable service. Let’s test it. 😃

There are multiple ways to test such services. We can use the classic “subscribe and assert” pattern or “marble testing.” If you want to find out more about their differences and which you should use, I recommend you to check out this article:

We are interested in marble testing this service.

Marble test a subject 🧪

To get started, we first instantiate our sut(system under test) and the TestScheduler.

Image for post
Image for post
Instantiate the class to test and the TestScheduler in a beforeEach hook

When instantiating the TestScheduler we pass a assertDeppEqual function which tells the TestScheduler how to compare values. From here on, we can start using the TestScheduler to write our marble test.

Image for post
Image for post

We want to test that the correct Pizza is emitted by our pizza$ Subject.

We use destructuring to get a hold of the TestScheduler’s expectObservable and cold helper methods. We then use the expectObservable function in combination with the Marble syntax to ensure that our expected Observable is an Observable that streams one value, which is “Tonno.”

Easy, isn’t it? Let’s run it.

Error: expect(received).toEqual(expected) // deep equality- Expected  - 11
+ Received + 1
- Array [
- Object {
- "frame": 0,
- "notification": Notification {
- "error": undefined,
- "hasValue": true,
- "kind": "N",
- "value": "Tonno",
- },
- },
- ]
+ Array []

Oh no. 😲 Our test fails. What happened?

If you like this blog post and want to get updated about new cool things that happen in modern frontend development — follow me on Twitter.

Let’s analyze this. The error message says that we expected a value of “Tonno” to be emitted at frame 0, but we didn’t receive any value. Why is that?

Let’s take a closer look at the function calls and the order in which they are executed.

  1. We call the orderPizza function in our test
sut.orderPizza(pizzaTonno);

2. The pizzaTonno gets “nexted” into our Subject.

this.pizza$.next(pizza);

3. Our test subscribes to the exposed Subject

expectObservable(sut.pizza$).toBe('a', {a: pizzaTonno});

From this order, we can see that it's a timing issue. The value is “nexted” into the Subject before it’s subscribed. After we subscribed, no value is streamed. Therefore we receive an empty array.

We need to bridge the gap between the timing of function calls and Observables.

Bridging the gap 🌉

To adjust this test, we need to change the timing of the orderPizza function call. Instead of calling it immediately, we need to call it in the same frame as the subscription.

To do so, we can use the TestScheduler’s cold helper method. As the name indicates, the cold function creates a cold Observable from a marble Diagram. We can subscribe to such an Observable and call the orderPizza function.

cold('-a').subscribe(() => sut.orderPizza(pizzaTonno))

Our complete test then looks like this:

Image for post
Image for post
✅ Successfull marble test a RxJs Subject

We bridged the gap, and our test is now green. We successfully marble tested a simple service with a Subject.

For sure, in this simple example, it would also be possible to use the “subscribe and assert” pattern. But then we would be forced to deal with the subscription and the done callback.

It’s up to you to decide which approach is more concise and easier to understand.

Further resources about Marble testing

If you want to get to know more in-depth detail on marble testing and the TestScheduler you may be interested in one of my articles about RxJS and testing.

🧞‍ 🙏 If you liked this post, share it and give some claps👏🏻 by clicking multiple times on the left side's clap button.

Claps help other people to discover content and motivate me to write more. 😉

Passionate freelance frontend engineer. ❤️ Always eager to learn, share and expand knowledge.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store