erin
erin

Reputation: 665

Unit testing value of Observable returned from service (using async pipe)

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?

Upvotes: 12

Views: 18197

Answers (2)

vince
vince

Reputation: 8306

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 :)

Upvotes: 15

Pavel Agarkov
Pavel Agarkov

Reputation: 3783

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.

Upvotes: 2

Related Questions