Javascript is required
·
9 min read

How I Write Marble Tests For RxJS Observables In Angular

How I Write Marble Tests For RxJS Observables In Angular Image

I am a passionate Reactive Extensions user and mostly use them in RxJS, which is integrated into the Angular framework.

In Angular, I often use observables in my services and need to write tests for these asynchronous data streams.

Unfortunately, testing observables is hard, and to be honest, I often need more time to write unit tests for my streams than to implement them in the production code itself. But luckily, there exists an integrated solution for RxJS which helps write this kind of test: the so-called marble tests.

Marble testing is not difficult if you are already familiar with representing asynchronous data streams as marble diagrams. In this blog post, I want to introduce you to the concept of marble diagrams, the basics of marble testing, and examples of how I use them in my projects.

I will not provide a general introduction to RxJS in this article, but I can highly recommend this article to refresh the basics.

Marble Testing

There exists an official documentation about marble testing for RxJS users but it can be tough to get started since there were a lot of changes from v5 to v6. Therefore I want to start by explaining the basics, show an exemplary Angular test implementation and in the end, talk about some of the new RxJS 6 features.

Marble Diagrams

For easier visualization of RxJS observables, a new domain-specific language called "marble diagram" was introduced.

Marble Diagrams are visual representations of how operators work and include the input Observable(s), the operator and its parameters, and the output Observable.

The following image from the official documentation describes the anatomy of a marble diagram:

Angular Marble Diagram Anatomy

In a marble diagram, time flows to the right, and the diagram describes how values (“marbles”) are emitted on the Observable execution.

Marble Syntax

In RxJS marble tests, the marble diagrams are represented as a string containing a special syntax representing events happening over virtual time. The start of time (also called the zero frame) in any marble string is always represented by the first character in the string.

  • - time: 1 "frame" of time passage.
  • | complete: The successful completion of an observable. This is the observable producer signaling complete().
  • # error: An error terminating the observable. This is the observable producer signaling error().
  • "a" any character: All other characters represent a value being emitted by the producer signaling next().
  • () sync groupings: When multiple events need to be in the same frame synchronously, parentheses are used to group those events. You can group nested values, a completion or an error in this manner. The position of the initial ( determines the time at which its values are emitted.
  • ^ subscription point: (hot observables only) shows the point at which the tested observables will be subscribed to the hot observable. This is the "zero frame" for that observable, every frame before the ^ will be negative.

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 for 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.

A Practical Angular Example

As you now know the theoretical basis, I want to show you a real-world Angular example.

In this GitHub repository, I have implemented a basic test setup which I will now explain in detail. The Angular CLI project consists of these components and services:

UserService

This service provides a public getter getUsers(), which returns an Observable that emits a new username each second.

user.service.ts
1import { Injectable } from '@angular/core'
2import { Observable, interval } from 'rxjs'
3import { take, map } from 'rxjs/operators'
4
5@Injectable({
6  providedIn: 'root',
7})
8export class UserService {
9  private readonly testData = ['Anna', 'Bert', 'Chris']
10
11  get getUsers(): Observable<string> {
12    return interval(1000).pipe(
13      take(this.testData.length),
14      map((i) => this.testData[i])
15    )
16  }
17}

AllMightyService

This service injects the above introduced UserService and provides the public getter getModifiedUsers. This getter also returns an Observable and maps the emitted usernames from userService.getUsers to make them more "mighty".

all-mighty.service.ts
1import { Injectable } from '@angular/core'
2import { map } from 'rxjs/operators'
3import { Observable } from 'rxjs'
4
5import { UserService } from './user.service'
6
7@Injectable({
8  providedIn: 'root',
9})
10export class AllMightyService {
11  get getModifiedUsers(): Observable<string> {
12    return this.userService.getUsers.pipe(map((user) => `Mighty ${user}`))
13  }
14
15  constructor(private userService: UserService) {}
16}

AppComponent

In our app.component.ts, we inject the UserService and update a list each time a new username is emitted from the getUsers Observable.

app.component.ts
1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { Subscription } from 'rxjs'
3
4import { UserService } from './services/user.service'
5
6@Component({
7  selector: 'app-root',
8  templateUrl: './app.component.html',
9  styleUrls: ['./app.component.scss'],
10})
11export class AppComponent implements OnInit, OnDestroy {
12  title = 'MarbleDemo'
13
14  users: string[] = []
15
16  private subscription: Subscription | undefined
17
18  constructor(private userService: UserService) {}
19
20  ngOnInit() {
21    this.subscription = this.userService.getUsers.subscribe((user) => {
22      this.users.push(user)
23    })
24  }
25
26  ngOnDestroy() {
27    if (this.subscription) {
28      this.subscription.unsubscribe()
29    }
30  }
31}
app.component.html
1<div style="text-align:center"><h1>Welcome to {{ title }}!</h1></div>
2<h2>Here will users pop in asynchronously:</h2>
3<ul>
4  <li class="user" *ngFor="let user of users"><h2>{{user}}</h2></li>
5</ul>

Now we can write different unit tests for this project:

  • Test that the AppComponent shows the correct list of usernames
  • Test that the AllMightyService correctly maps and emits the usernames

Let us start with the unit test for the AppComponent.

In these tests, I am using the npm package jasmine-marbles which is a helper library that provides a neat API for marble tests if you are using jasmine (which is used per default in Angular).

Basic idea is to mock the public observables from the provided services and test our asynchronous data streams in a synchronous way.

We mock the UserService and the getUsers observable. In the test case we flush all observables by calling