I have the following service:
export class MathService {
private _total = new BehaviorSubject(0);
total$ = this._total.asObservable();
add(num: number) {
this._total.next(this._total.value() num);
}
subtract(num: number) {
this._total.next(this._total.value() - num);
}
}
How would you test that total$
emits the correct values in a sequence of add
and subtract
function calls like this:
service.add(10) // should emit 10;
service.subtract(3) // should emit 7;
service.add(20) // should emit 27;
service.subtract(5) // should emit 22;
...
Would marble test work for something like this? If so, how would you set that up? I wasn't able to find a clear example online of how to test that an observable on a service emits the proper sequence of values given a sequence of function calls on that service?
CodePudding user response:
First of all I would try to test without marble diagrams, just to make sure we understand how the asyn execution would work.
it('should test the Observable', () => {
// create the instance of the service to use in the test
const mathService = new MathService();
// define the constant where we hold the notifications
const result: number[] = [];
const expected = [0, 0, 0, 1, 0, 2, 0]; // expected notifications
// this is a sequence of adds
const add$ = timer(0, 100).pipe(
take(3),
tap((i) => {
console.log('add', i);
mathService.add(i);
}),
);
// this is a sequence of subtracts, which starts 50 ms after the adds
const subtract$ = timer(50, 100).pipe(
take(3),
tap((i) => {
console.log('sub', i);
mathService.subtract(i);
}),
);
// here we subscribe to total$ and we store any notification in the result array
mathService.total$.subscribe({
next: (s) => {
result.push(s);
},
});
// here we merge adds and subtracts and, at completion, we check which are the notifications
// we have saved in the result array
merge(add$, subtract$).subscribe({
complete: () => {
console.log('===>>>', result, expected);
},
});
});
Once the async mechanism is clear, then we can look at an implementation which uses marble diagrams, like this one
let testScheduler: TestScheduler;
beforeEach(() => {
testScheduler = new TestScheduler(observableMatcher);
});
it.only('should test the Observable', () => {
testScheduler.run(({ hot, expectObservable }) => {
const mathService = new MathService();
const add = hot(' --0-----1---2---------');
const subtract = hot(' ----0-----1---2-------');
const expected = ' --0-0---1-0-2-0-------';
const _add = add.pipe(tap((i) => mathService.add(parseInt(i))));
const _subtract = subtract.pipe(tap((i) => mathService.subtract(parseInt(i))));
const result = merge(_add, _subtract).pipe(
concatMap((val) => {
console.log('val', val);
return mathService.total$.pipe(map((v) => v.toString()));
}),
);
expectObservable(result).toBe(expected);
});
});
This implementation follows some examples of tests used in the rxJs library.
The implmentation of observableMatcher
can be seen here.