In my angular project, I have a service, which is used for state management to share some data between components as following:
@Injectable({ providedIn: "root"})
export class NameStateService {
private _filteredNames$: Subject<Name[]> = new Subject();
private _filteredNamesObs$: Observable<Name[]>;
constructor() {
this._filteredNamesObs$ = this._filteredNames$.asObservable();
}
public updateFilteredNames(val: Name[]): void {
this._filteredNames$.next(val);
}
public get filteredNames$(): Observable<BillingAccount[]> {
return this._filteredNamesObs$;
}
}
the state management is based on subject and observable, which is typical usage in rxjs world.
And For the unit test for this service, I want to use the marble testing features suppored by rxjs/testing
module. The solution goes as following:
describe("Name state service ", () => {
let nameStateService: NameStateService;
let scheduler: TestScheduler;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
NameStateService
]
});
nameStateService = TestBed.get(NameStateService);
scheduler = new TestScheduler((actual, expected) => expect(actual).toEqual(expected));
});
it("should be created", () => {
expect(nameStateService).toBeTruthy();
});
it("should return a valid names observables", () => {
scheduler.run(({ cold, hot, expectObservable }) => {
const mockNames: Name[] = [{
title: "title1",
group: "group1"
}];
nameStateService.updateFilteredNames(mockNames);
expectObservable(nameStateService.filteredNames$).toBe("-b", {b: mockNames});
});
});
})
But the second unit test failed with the error: Error: Expected $.length = 0 to equal 1.
So it means that nameStateService.filteredNames$
, this observable has no values in its stream.
What is the issue here?
Your set up could be quite hard to test using RxJS marbles.
The first issue is that you're sending some stuff down the stream before subscribing to it. Remember,
expectObservable()
synchronously subscribes to a passed observable (in your casenameStateService.filteredNames$
). But, no data is sent there since you've already sent it by callingnameStateService.updateFilteredNames(mockNames)
.You could think about having these two lines change positions, but remember that this is synchronous execution environment, so doing this
wouldn't help either since
expectObservable()
would subscribe tonameStateService.filteredNames$
, then it would read all the values, but since there's no values as you're sending them in a line after this, anactual
array would be empty.To avoid this, you should mock your
nameStateService.filteredNames$
observable. To do it, you could do two things, but both of them having their own issues. So, the second issue with your set up is that you could makecold
orhot
Observable and use them instead offilteredNames$
Observable.This could be achieved like this:
but this errors with TS2540: Cannot assign to 'filteredNames$' because it is a read-only property. since you don't have setter for
filteredNames$
. If you'd add setter, then this would break your contract of havingthis._filteredNamesObs$
as private property that is created fromthis._filteredNames$
Subject
.The other way would be to mock
this._filteredNames$
using Jasmine spies (which is a third issue), but this set up is also having problems. What to mock? The wholenameStateService
? In this case, you'd have to create spies for every particular property and function of the service. Or mocknameStateService._filteredNames$
property? Or even better,nameStateService.filteredNames$
? But mocking them would cause others to behave differently since not all of them are mocked.So, I would suggest not using TestScheduler at all, and writing your test like this:
However, another solution could be present (if you really, really want to use marbles), you'd just have to be very careful and know what you're doing. You could change
_filteredNames$
to aReplaySubject
.This would cause any subsequent subscribers to get all the values that are sent down the stream even before subscribing to it. You'd just have to delete one char (
-
sign that is passed totoBe
method, and your passing test would look like this: