The problem
I'm trying to find a way of using marble testing to test side efects with async pipes. I've created a simple POC in Stackblitz so you may test it for yourselves https://stackblitz.com/edit/angular-ivy-pzbtqx?file=src/app/app.component.spec.ts
I'm using the result from a service method, which returns an Observable
of either an object or null (see component file), and the *ngIf
directive with an async
pipe to either display or hide some html element depending wether the result from the method was an object or null (see html file).
Now I would like to create a Unit Test for the aforementioned case using marble testing however when I use the cold
observable as the return value from my mocked service. It is allways being interpreted as null (or to be more exact falsy) by the async
pipe.
html
<h1>Marble Tests POC</h1>
<div id="conditional" *ngIf="value$ | async">
<p>My Conditional message!</p>
</div>
component
export class AppComponent implements OnInit {
value$: Observable<{} | null>;
constructor(private myService: MyService) {}
ngOnInit(): void {
this.value$ = this.myService.getValue(false);
}
}
spec
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let mockedMyService = new MyServiceMock();
let getValueSpy: jasmine.Spy;
beforeEach(() => {
getValueSpy = spyOn(mockedMyService, 'getValue').and.returnValue(of({}));
});
beforeEach(async () => {
// module definition providers and declarations...
});
beforeEach(() => {
// fixture and component initialization...
});
it('should display message when service returns different than null', () => {
const testCase$ = cold('a', { a: {} });
// if you comment the following line or use a normal Observable [of({})] instead of the
// coldObservable the test passes without issues.
getValueSpy.and.returnValue(testCase$);
component.ngOnInit();
getTestScheduler().flush();
fixture.detectChanges();
const conditionalComponent = fixture.debugElement.query(
By.css('#conditional')
);
expect(conditionalComponent).not.toBeNull(); // Expected null not to be null.
});
});
Possible explanation:
I'm thinking the issue is that the async
pipe seems not to work with ColdObservable
s at all or at least it seems to be working in a different way than with normal Observable
s. Now I know this can be tested without marble testing; that is the old way with fakeAsync
or done
function, but I would love to use marble testing since is way simpler to reason about.
Background
I came up with this idea from the example given on the Angular - Component testing scenarios documentation which gives the following testCase with jasmine-marbles:
it('should show quote after getQuote (marbles)', () => {
// observable test quote value and complete(), after delay
const q$ = cold('---x|', { x: testQuote });
getQuoteSpy.and.returnValue( q$ );
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent)
.withContext('should show placeholder')
.toBe('...');
getTestScheduler().flush(); // flush the observables
fixture.detectChanges(); // update view
expect(quoteEl.textContent)
.withContext('should show quote')
.toBe(testQuote);
expect(errorMessage())
.withContext('should not show error')
.toBeNull();
});
As you can see. they use the flush()
method to run the coldObservable
and then use the detectChanges()
method to update the view.
P.S.
Before someone links Jasmine marble testing observable with ngIf async pipe as duplicate please note that question does not have a good answer and the OP did not post a comprehensive solution to his problem
Thanks to akotech for the answer I'm providing bellow!
Solution:
Explanation:
We are modifying the returned
Observable
from the mocked service.In the
beforeEach
callback we are returning anObservable
from theof
operator (we will call thisObs1
). Then we are modifying this return value on the actual test returning now theTestColdObservable
(we shall call thisObs2
).We know that the first thing to be executed before our tests is the
beforeEach
callback and in case of multiple callbacks they are executed in order. So first we set the mock to returnObs1
then we callcreateComponent()
anddetectChanges()
which in turn invokesngOnInit()
and refreshes the view respectively. When the view is refreshed theasync
pipe subscribes to theObs1
returned by the mock.After executing the
beforeEach
callback. We start executing the actual test and the first thing we do is modify the returned value of the mock to now returnObs2
. Then we call thengOnInit
method to change the observablevalue$
so it points toObs2
. However, instead of updating the view so theasync
pipe updates it's subscription toObs2
. We proceeded toflush
the observables leaving theasync
pipe pointing toObs1
rather thanObs2
;Diagram:
Original
Fixed