2. Agenda
What will be presented?
● Marble diagrams
● Marble testing RxJS Observables
● Angular examples
3. Why?
Asynchronous code is hard to
understand and to test.
Reactive processing using
Observables and Subscribers is no
exception.
Marble diagrams and marble tests
may help in this particular case.
4. What a marble
diagram is?
Particularly useful for representing
reactive streams (observables) and
their operators.
Can depict the output of multiple
asynchronous observables in a given
time frame.
13. ReactiveX Zip operator http://reactivex.io/documentation/operators/zip.html
Representation of time is
important to show how an
operator works
14. What is a marble
test?
● Test that use mocked
observables expressed by
marbles.
● Time is simulated: observables
will progress synchronously, in
fixed time frames, according
with their marble descriptions.
15. Marble testing the Zip operator
Given two source observables represented by these marbles
When the Zip operator is applied to them
It should result in an observable represented by this marble
16. Marble testing the Zip operator
Given two source observables represented by these marbles
When the Zip operator is applied to them
It should result in an observable represented by this marble
T1 T2
Simulated
Time slots
17. Marble testing the Zip operator
it('should zip elements', () => {
// Given two source and a result observable represented by these marbles
const numbers$: Observable<number> = cold(' -1-2-----3-4--5-|');
const chars$: Observable<string> = cold(' --A-B--CD-------|');
const expected$: Observable<[string, string]> = cold('--X-Y----W-Z----|', {
X: ['1', 'A'],
Y: ['2', 'B'],
W: ['3', 'C'],
Z: ['4', 'D'] // emitted objects have to be mapped to the symbols in the marble
});
// When the zip operator is applied to the source marbles
const actual$ = zip(numbers$, chars$);
getTestScheduler().flush();
// The actual result observable should be equal to the expected marble
expect(actual$).toBeObservable(expected$);
});
19. Angular examples
● Test the RandomSpoilerComponent,
that asynchronously loads a random
spoiler for the view.
● Test the SpoilerService, that gets a
list of spoilers from a REST service
and returns a random spoiler.
20. Marble testing an Angular component
export class RandomSpoilerComponent {
spoiler: Spoiler; // property displayed by the view
constructor(private spoilerService: SpoilerService, private snackBar: MatSnackBar) { }
/* Called when user clicks the button. Gets a random spoiler observable from the spoilerService.
If it receives a new spoiler, assign it to the 'spoiler' property.
If it receives an error, display an error message. */
nextSpoiler() {
this.spoilerService.getRandomSpoiler().pipe(take(1)).subscribe({
next: (newSpoiler) => {
this.spoiler = newSpoiler;
},
error: () => {
this.snackBar.open('It was not possible to load your next spoiler.', null, {duration: 3000});
}});
}
}
21. Marble testing an Angular component
it('should load a new spoiler', () => {
// given the spoilerService returns a single spoiler, represented by a marble
const spoiler = { id: 1, spoiler: 'Bruce Willis is dead' };
const serviceSpoiler$ = cold('----a', {a: spoiler}); // delayed to ensure async response is handled
spyOn(spoilerService, 'getRandomSpoiler').and.returnValue(serviceSpoiler$);
// when the component loads the next random spoiler
component.nextSpoiler();
getTestScheduler().flush();
// then the new spoiler should become available for the view in a property
expect(component.spoiler).toBe(spoiler);
});
22. Marble testing an Angular component
it('should display a message message when an error occurs while getting a spoiler', () => {
// given the spoilerService returns an error
const serviceSpoiler$ = cold('----#'); // in ascii marbles errors are represented by a '#'
spyOn(spoilerService, 'getRandomSpoiler').and.returnValue(serviceSpoiler$);
spyOn(snackBar, 'open');
// when the component tries to load the next random spoiler
component.nextSpoiler();
getTestScheduler().flush();
// then it should display an error message in a 'snack bar' notification
expect(snackBar.open).toHaveBeenCalledWith(
'It was not possible to load your next spoiler.', null, {duration: 3000});
});
23. Marble testing an Angular service
export class SpoilerService {
constructor(private httpClient: HttpClient) { }
// Requests a list of spoilers from a REST service map it to return a single random spoiler
public getRandomSpoiler(): Observable<Spoiler> {
const uri = 'http://localhost:3000/spoilers/';
return this.httpClient.get<Spoiler[]>(uri) // [#1, #2, #3]
.pipe(
map(spoilers => spoilers[this.randomIndex(spoilers)]) // #X
);
}
randomIndex(spoilers) {
return Math.floor(Math.random() * spoilers.length);
}
}
24. Marble testing an Angular service
it('should get a random spoiler', () => {
// given the HTTP request will return a list of spoilers
const spoiler1: Spoiler = { id: 1, spoiler: 'Bruce Willis is dead'};
const spoiler2: Spoiler = { id: 2, spoiler: "Brad Pitt is Edward Norton's alter ego"};
const spoilers$ = cold('---L', {L: [spoiler1, spoiler2]});
const expectedRandomSpoiler$ = cold('---a', {a: spoiler2} );
spyOn(httpClient, 'get').and.returnValue(spoilers$);
spyOn(service, 'randomIndex').and.returnValue(1);
// when we get a random spoiler
const actualRandomSpoiler$ = service.getRandomSpoiler();
getTestScheduler().flush();
// then the service must return a single random spoiler in an observable
expect(actualRandomSpoiler$).toBeObservable(expectedRandomSpoiler$);
});