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