
How To Easily Write And Debug RxJS Marble Tests
Michael Hoffmann
@mokkapps

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:
const obs = `-a-^-b--|`
// 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:
Error:
+ Source: "--x-x--|"
- 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:
import { expect } from 'chai'
import { rxSandbox } from 'rx-sandbox'
it('testcase', () => {
const { hot, cold, flush, getMessages, e, s } = rxSandbox.create()
const e1 = hot(' --^--a--b--|')
const e2 = cold(' ---x--y--|', { x: 1, y: 2 })
const expected = e(' ---q--r--|')
const sub = s(' ^ !')
const messages = getMessages(e1.merge(e2))
flush()
//assertion
expect(messages).to.deep.equal(expected)
expect(e1.subscriptions).to.deep.equal(sub)
})
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
import { rxSandbox } from 'rx-sandbox'
import { AppFacadeService } from './app-facade.service'
import { NewsApiService, testData } from '../api/news-api.service'
describe('AppFacadeService', () => {
let sut: AppFacadeService
let newsApiService: any
let rx: any
beforeEach(() => {
// we need to create a sandbox for each test run
rx = rxSandbox.create()
const { cold, hot } = rx
// we mock the API service and return mocked observables which are created by marble strings
newsApiService = jasmine.createSpyObj('NewsApiService', ['fetchNews', 'connectToNewsStream'])
newsApiService.fetchNews.and.returnValue(
cold('a', {
a: testData,
})
)
newsApiService.connectToNewsStream.and.returnValue(
hot('a-^-a-b-c|', {
a: testData[0],
b: testData[1],
c: testData[2],
})
)
// we create a new instance of the service and pass the mock service to its constructor
sut = new AppFacadeService(newsApiService)
})
})
Marble Test
After creating the test setup we are now ready for our first test:
it('should return news from stream', () => {
const { e, getMessages, flush } = rx
// create the expected observable by using marble string
const expectedObservable = e('--a-b-c|', {
a: testData[0],
b: testData[1],
c: testData[2],
})
// get metadata from observable to assert with expected metadata values
const messages = getMessages(sut.connect())
// execute observables
flush()
// When assertion fails, 'marbleAssert' will display visual / object diff with raw object values for easier debugging.
marbleAssert(messages).to.equal(expectedObservable)
})
A failed test will show a similar output:
We can immediately see that the received observable emitted the events on different frames:
Error:
"Source: --a-b-c|"
"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:
{
"frame": 2, // at which frame the event occurred
"notification": {
"error": undefined, // any error information
"hasValue": true, // true if there is a value
"kind": "N", // type of the event, N: next, E: error, C: complete
"value": { // content of the next event
"author": "Mike",
"date": 2019-09-11T00:00:00.000Z,
"title": "New Xbox revealed"
}
}
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:
@@ -17,18 +17,18 @@
"notification": Notification {
"error": undefined,
"hasValue": true,
"kind": "N",
"value": Object {
- "author": "Chris",
- "date": 2019-12-12T00:00:00.000Z,
- "title": "Overwatch 5 announced",
+ "author": "Florian",
+ "date": 2019-05-12T00:00:00.000Z,
+ "title": "Halo X Review",
},
},
},
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.