Running Angular/Jasmine/Karma, I have a component that consumes a service to set the value of an Observable 'items' array. I display this using an async pipe. Works great.
Now, I'm trying to set up a unit test and got it to pass, but I'm not sure I'm correctly verifying that the 'items' array is getting the correct value.
Here is the relevant component .html and .ts :
export class ViperDashboardComponent implements OnInit, OnDestroy {
items: Observable<DashboardItem[]>;
constructor(private dashboardService: ViperDashboardService) { }
ngOnInit() {
this.items = this.dashboardService.getDashboardItems();
}
}
<ul class="list-group">
<li class="list-group-item" *ngFor="let item of items | async">
<h3>{{item.value}}</h3>
<p>{{item.detail}}</p>
</li>
</ul>
And my component.spec.ts:
beforeEach(() => {
fixture = TestBed.createComponent(ViperDashboardComponent);
component = fixture.componentInstance;
viperDashboardService =
fixture.debugElement.injector.get(ViperDashboardService);
mockItems = [
{ key: 'item1', value: 'item 1', detail: 'This is item 1' },
{ key: 'item2', value: 'item 2', detail: 'This is item 2' },
{ key: 'item3', value: 'item 3', detail: 'This is item 3' }
];
spy = spyOn(viperDashboardService, 'getDashboardItems')
.and.returnValue(Observable.of<DashboardItem[]>(mockItems));
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should call getDashboardItems after component initialzed', () => {
fixture.detectChanges();
expect(spy.calls.any()).toBe(true, 'getDashboardItems should be called');
});
it('should show the dashboard after component initialized', () => {
fixture.detectChanges();
expect(component.items).toEqual(Observable.of(mockItems));
});
Specifically, I'd like to know:
1) I started off creating an async "it" test, but was surprised when that didn't work. Why does a synchronous test work when I'm working with async data streams?
2) When I check the equivalence of component.items to the Observable.of(mockItems), am I really testing that the values are equal? Or am I just testing that they are both Observables? Is there a better way?
Angular's async pipe is a pipe that subscribes to an Observable or Promise for you and returns the last value that was emitted.
The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted. When a new value is emitted, the async pipe marks the component to be checked for changes. When the component gets destroyed, the async pipe unsubscribes automatically to avoid potential memory leaks.
If an operation is asynchronous just because it relies on setTimeout or other time-based behavior, a good way to test it is to use Jasmine's mock clock to make it run synchronously. This type of test can be easier to write and will run faster than an asynchronous test that actually waits for time to pass.
Angular's async pipe is a tool to resolve the value of a subscribable in the template. A subscribable can be an Observable , an EventEmitter , or a Promise . The pipe listens for promises to resolve and observables and event emitters to emit values. Let's take a look at how we can profit from using the async pipe.
Angular provides the utilities for testing asynchronous values. You can use the async
utility with the fixture.whenStable
method or the fakeAsync
utility with the tick()
function. Then using the DebugElement
, you can actually query your template to be sure the values are getting loaded correctly.
Both methods for testing will work.
Using the async
utility with whenStable
:
Keep your setup the same, you're good to go there. You'll need to add some code to get ahold of the list's debug element. In your beforeEach
:
const list = fixture.debugElement.query(By.css('list-group'));
Then you can dig in to that list and get individual items. I won't go too far into how to use DebugElement
because that's outside the scope of this question. Learn more here: https://angular.io/guide/testing#componentfixture-debugelement-and-querybycss.
Then in your unit test:
it('should get the dashboard items when initialized', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for your async data
fixture.detectChanges(); // refresh your fake template
/*
now here you can check the debug element for your list
and see that the items in that list correctly represent
your mock data
e.g. expect(listItem1Header.textContent).toEqual('list item 1');
*/
}
}));
Using the fakeAsync
utility with tick
:
it('should get the dashboard items when initialized', fakeAsync(() => {
fixture.detectChanges();
tick(); // wait for async data
fixture.detectChanges(); // refresh fake template
/*
now here you can check the debug element for your list
and see that the items in that list correctly represent
your mock data
e.g. expect(listItem1Header.textContent).toEqual('list item 1');
*/
}));
So in summary, do not remove the async
pipe from your template just to make testing easier. The async
pipe is a great utility and does a lot of clean up for you, plus the Angular team has provided some very useful testing utilites for this exact use-case. Hopefully one of the techniques above works. It sounds like using DebugElement
and one of the aforementioned utilities will help you lots :)
There is a package for testing observables jasmine-marbles
With it you can write test like this:
...
spy = spyOn(viperDashboardService, 'getDashboardItems')
.and.returnValue(cold('-r|', { r: mockItems }));
...
it('should show the dashboard after component initialized', () => {
const items$ = cold('-r|', { r: mockItems });
expect(component.items).toBeObservable(items$);
});
But probably it is not the best example - I normally do use this package to test chains of observables. For example if I have service that do inside some .map()
ing with input observable - I can mock original observable and then create a new one and compare with service results.
Also neither async
nor fakeAsync
will work with time dependent observable operations, but with jasmine-marbles
you can inject test scheduler into them and it will work like a charm and without any timeouts - instantly! Here I have an example how to inject a test scheduler.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With