End of 2018 I wrote an article about how I write marble tests for RxJS observables in Angular. The content in there is still valid but I found recently a new library which I like and which makes debugging marble tests easier.
If you do not know RxJS marble tests yet then I would recommend you to first read my article which covers the basics.
As quick catchup, the following example shows a marble diagram which 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 which is 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 really easy and understandable representation of what went wrong in the test.
The library also has some other nice features:
- No dependencies to 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 which simulates an API communication to fetch news -
AppFacadeService
: 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 emitted values from source and expected observable 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.
Top comments (1)
Thanks, this is extremely helpful.