How To Easily Write And Debug RxJS Marble Tests
End of 2018, I wrote an article about how I write marble tests for RxJS observables in Angular. The content is still valid, but I recently found a new library that I like and makes debugging marble tests easier.
If you do not know RxJS marble tests yet, I recommend you first read my article, which covers the basics.
As quick catchup, the following example shows a marble diagram that can be used in tests to represent an observable:
1const obs = `-a-^-b--|`
2// 012345`, emits 'b' on frame 2, completes on 5 - hot observable ^ represents when the subscription started
In this article, I want to talk about rx-sandbox, a marble diagram DSL-based test suite for RxJS 6. It also has support for RxJS 5 in pre-1.x versions if you need that in your application.
Why rx-sandbox?
I found this library as I was looking for a better way to debug marble tests as it was not possible to see such a test output using the jasmine-marbles library:
1Error:
2+ Source: "--x-x--|"
3- Expected: "---x-x--|"
In my opinion, this is a straightforward and understandable representation of what went wrong in the test.
The library also has some other nice features:
- No dependencies on a specific test framework.
- Near-zero configuration, works out of box.
- Supports extended marble diagram DSL.
- Provides feature parity to TestScheduler.
Hello World Example
This is simple example of a marble test using rx-sandbox from the official GitHub repository:
1import { expect } from 'chai'
2import { rxSandbox } from 'rx-sandbox'
3
4it('testcase', () => {
5 const { hot, cold, flush, getMessages, e, s } = rxSandbox.create()
6 const e1 = hot(' --^--a--b--|')
7 const e2 = cold(' ---x--y--|', { x: 1, y: 2 })
8
9 const expected = e(' ---q--r--|')
10 const sub = s(' ^ !')
11
12 const messages = getMessages(e1.merge(e2))
13
14 flush()
15
16 //assertion
17 expect(messages).to.deep.equal(expected)
18 expect(e1.subscriptions).to.deep.equal(sub)
19})
More Realistic Example
As things are typically more complicated than in the simple examples, I have created a project which contains a more realistic scenario with this simple architecture:
The demo application contains these services:
NewsApiService
: Represents a service that simulates an API communication to fetch newsAppFacadeService
: The facade which is used betweenAppComponent
andNewsApiService
to handle the communication and add additional functionality on top of the API calls
The relevant marble tests are located in app-facade.service.spec.ts.
Create Test Instance
1import { rxSandbox } from 'rx-sandbox'
2
3import { AppFacadeService } from './app-facade.service'
4import { NewsApiService, testData } from '../api/news-api.service'
5
6describe('AppFacadeService', () => {
7 let sut: AppFacadeService
8 let newsApiService: any
9 let rx: any
10
11 beforeEach(() => {
12 // we need to create a sandbox for each test run
13 rx = rxSandbox.create()
14 const { cold, hot } = rx
15
16 // we mock the API service and return mocked observables which are created by marble strings
17 newsApiService = jasmine.createSpyObj('NewsApiService', ['fetchNews', 'connectToNewsStream'])
18 newsApiService.fetchNews.and.returnValue(
19 cold('a', {
20 a: testData,
21 })
22 )
23 newsApiService.connectToNewsStream.and.returnValue(
24 hot('a-^-a-b-c|', {
25 a: testData[0],
26 b: testData[1],
27 c: testData[2],
28 })
29 )
30
31 // we create a new instance of the service and pass the mock service to its constructor
32 sut = new AppFacadeService(newsApiService)
33 })
34})
Marble Test
After creating the test setup we are now ready for our first test:
1it('should return news from stream', () => {
2 const { e, getMessages, flush } = rx
3
4 // create the expected observable by using marble string
5 const expectedObservable = e('--a-b-c|', {
6 a: testData[0],
7 b: testData[1],
8 c: testData[2],
9 })
10
11 // get metadata from observable to assert with expected metadata values
12 const messages = getMessages(sut.connect())
13
14 // execute observables
15 flush()
16
17 // When assertion fails, 'marbleAssert' will display visual / object diff with raw object values for easier debugging.
18 marbleAssert(messages).to.equal(expectedObservable)
19})
A failed test will show a similar output:
We can immediately see that the received observable emitted the events on different frames:
1Error:
2"Source: --a-b-c|"
3"Expected: --a-b---c|"
Additionally, the frames may be correct, but the source and expected observable values differ.
The output for each event is in this format:
1{
2 "frame": 2, // at which frame the event occurred
3 "notification": {
4 "error": undefined, // any error information
5 "hasValue": true, // true if there is a value
6 "kind": "N", // type of the event, N: next, E: error, C: complete
7 "value": { // content of the next event
8 "author": "Mike",
9 "date": 2019-09-11T00:00:00.000Z,
10 "title": "New Xbox revealed"
11 }
12}
So you will then compare these values from the received and expected observables. rx-sandbox will print you a diff to see the difference in the values:
1@@ -17,18 +17,18 @@
2 "notification": Notification {
3 "error": undefined,
4 "hasValue": true,
5 "kind": "N",
6 "value": Object {
7- "author": "Chris",
8- "date": 2019-12-12T00:00:00.000Z,
9- "title": "Overwatch 5 announced",
10+ "author": "Florian",
11+ "date": 2019-05-12T00:00:00.000Z,
12+ "title": "Halo X Review",
13 },
14 },
15 },
Conclusion
In my experience, most developers struggle with interpreting the result of marble tests as libraries like jasmine-marbles
do not provide a good visual representation of the expected and received streams.
rx-sandbox
solves this problem by providing a visual representation of the expected & received marble string and a more readable diff of the values. Additionally, you can use the library in any frontend test framework.
Let me know your thoughts about this library in the comments.
The Mistakes I Made In My First Software Project
Before starting my professional career as a developer, I mainly developed Android apps using Java as the programming language. I got hired by a software service company, and we had to develop JavaScript-based applications for cars in my first project. So the first time in my life, I had to work with JavaScript, and I made many mistakes during this time which I now want to share with you.
JHipster - The Fastest Way To Build A Production-Ready Angular & Spring Boot Application
In the last years, I mainly worked on the frontend part of web & mobile applications, but I also did some minor backend work. Since mid of this year, I have been working to improve my backend knowledge and started to focus on Java backend development using Spring Boot.