Javascript is required
·
9 min read
·
21309 views

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 getTestScheduler().flush(). This means that after this line has been executed our mocked observable has emitted all of its events and we can run our test assertions. I will talk more about the TestScheduler after this example.

app.component.spec.ts
1import { TestBed, async } from '@angular/core/testing'
2import { getTestScheduler, cold } from 'jasmine-marbles'
3
4import { AppComponent } from './app.component'
5import { UserService } from './services/user.service'
6import { By } from '@angular/platform-browser'
7
8describe('AppComponent', () => {
9  let userService: any
10
11  beforeEach(async(() => {
12    // Here we mock the UserService to a cold Observable emitting three names
13    userService = jasmine.createSpy('UserService')
14    userService.getUsers = cold('a-b-c', { a: 'Mike', b: 'Flo', c: 'Rolf' })
15
16    TestBed.configureTestingModule({
17      declarations: [AppComponent],
18      providers: [{ provide: UserService, useValue: userService }],
19    }).compileComponents()
20  }))
21
22  it('should correctly show all user names', async () => {
23    const fixture = TestBed.createComponent(AppComponent)
24    fixture.detectChanges() // trigger change detection
25
26    getTestScheduler().flush() // flush the observable
27    fixture.detectChanges() // trigger change detection again
28
29    const liElements = fixture.debugElement.queryAll(By.css('.user'))
30    expect(liElements.length).toBe(3)
31
32    expect(liElements[0].nativeElement.innerText).toBe('Mike')
33    expect(liElements[1].nativeElement.innerText).toBe('Flo')
34    expect(liElements[2].nativeElement.innerText).toBe('Rolf')
35  })
36})

In the next step, let us analyze a service test, in this case for the AllMightyService.

all-mighty.service.spec.ts
1import { hot, cold } from 'jasmine-marbles'
2import { TestScheduler } from 'rxjs/testing'
3
4import { AllMightyService } from './all-mighty.service'
5import { fakeAsync } from '@angular/core/testing'
6
7describe('AllMightyService', () => {
8  let sut: AllMightyService
9  let userService: any
10
11  beforeEach(() => {
12    // we mock the getUsers Observable of the UserService
13    userService = jasmine.createSpy('UserService')
14    userService.getUsers = hot('^-a-b-c', {
15      a: 'Hans',
16      b: 'Martin',
17      c: 'Julia',
18    })
19
20    sut = new AllMightyService(userService)
21  })
22
23  it('should be created', () => {
24    expect(sut).toBeTruthy()
25  })
26
27  it('should correctly return mighty users (using jasmine-marbles)', () => {
28    // Here we define the Observable we expect to be returned by "getModifiedUsers"
29    const expectedObservable = cold('--a-b-c', {
30      a: 'Mighty Hans',
31      b: 'Mighty Martin',
32      c: 'Mighty Julia',
33    })
34    expect(sut.getModifiedUsers).toBeObservable(expectedObservable)
35  })
36})

The TestScheduler

As we already saw in the first AppComponent test, RxJS provides a TestScheduler for "time manipulation".

The internal schedulers control the emission order of events in RxJS. Most of the time, we do not have to care about the schedulers as they are handled mainly by RxJS internally. But we can provide a scheduler to operators, as we can see in the signature of the "delay" operator:

delay(delay: number | Date, scheduler: Scheduler): Observable

The last parameter is optional and defaults to the async Scheduler. RxJS includes the following Schedulers:

  • AsyncScheduler
  • AnimationFrameScheduler
  • AsapScheduler
  • QueueScheduler
  • TestScheduler
  • VirtualTimeScheduler

To avoid using real time in our test, we can pass the TestScheduler (who derives from the VirtualTimeScheduler) to our operator. The TestScheduler allows us to manipulate the time in our test cases and synchronously write asynchronous tests.

New RxJS 6 marble test features

In RxJS v5 there was nearly no documentation for the TestScheduler as it was mainly used internally by the library authors. Since RxJS 6 this has changed and we can now use the TestScheduler to write marble tests.

testScheduler.run(callback)

In previous RxJS versions, we had to pass the Scheduler to our operators in production code to be able to test them with virtual time manipulation.

1getUsers(scheduler) {
2    const dummyData = Observable.from(['Anna', 'Bert', 'Chris']);
3    return dummyData.delay(1000, scheduler); // each user is emitted after 1 second
4}

As you can see, we are now mixing our productive code with logic, we only need for tests. The scheduler parameter is only added and used for the tests.

This issue is solved by the new run method. Every RxJS operator who uses the AsyncScheduler (for example "timer" or "debounce") will automatically use the TestScheduler when it is executed inside the run method and therefore uses virtual instead of real time.

This way, the same method above can be rewritten without the scheduler parameter and has no more test code inside the production code:

1getUsers() {
2    const dummyData = Observable.from(['Anna', 'Bert', 'Chris');
3    return dummyData.delay(1000); // each user is emitted after 1 second
4}

A unit test for the AllMightyService's getModifiedUsers method using the new run method can look this way:

1it('should correctly return mighty users (using RxJS 6 tools)', () => {
2  const scheduler = new TestScheduler((actual, expected) => {
3    // asserting the two objects are equal
4    expect(actual).toEqual(expected)
5  })
6
7  scheduler.run((helpers) => {
8    const { expectObservable } = helpers
9
10    const coldObservable = scheduler.createHotObservable('^-a-b-c', {
11      a: 'Hans',
12      b: 'Martin',
13      c: 'Julia',
14    })
15    userService.getUsers = coldObservable
16    sut = new AllMightyService(userService)
17
18    const expectedMarble = '--a-b-c'
19    const expectedVales = {
20      a: 'Mighty Hans',
21      b: 'Mighty Martin',
22      c: 'Mighty Julia',
23    }
24    expectObservable(sut.getModifiedUsers).toBe(expectedMarble, expectedVales)
25  })
26})

It looks the same as in our jasmine-marble test above, but the new run method provides some interesting new features like the Time progression syntax.

At this time the TestScheduler can only be used to test code that uses timers, like delay/debounceTime/etc (i.e. it uses AsyncScheduler with delays > 1). If the code consumes a Promise or does scheduling with AsapScheduler/AnimationFrameScheduler/etc it cannot be reliably tested with TestScheduler, but instead should be tested more traditionally. See the Known Issues section for more details.

Conclusion

Marble diagrams are an established concept to visualize asynchronous data, as seen on the popular website RxMarbles. Using marble strings, we can also use this clean way to test our observables.

I recommend getting started by using helper libraries like jasmine-marbles as they are more beginner-friendly. You can combine your jasmine-marble tests with the new RxJS 6 features in the same project I demonstrate in my example project.

From my experience, I can tell you that it is worth learning marble testing as you can then test very complex observable streams, understandably.

I hope that you are now able to start using marble tests in your project and that you will begin enjoying writing unit tests for observables.

I will never share any of your personal data. You can unsubscribe at any time.

If you found this article helpful.You will love these ones as well.
How To Easily Write And Debug RxJS Marble Tests Image

How To Easily Write And Debug RxJS Marble Tests

How To Generate Angular & Spring Code From OpenAPI Specification Image

How To Generate Angular & Spring Code From OpenAPI Specification

Manually Lazy Load Modules And Components In Angular Image

Manually Lazy Load Modules And Components In Angular

How To Build An Angular App Once And Deploy It To Multiple Environments Image

How To Build An Angular App Once And Deploy It To Multiple Environments