Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 412
erin Avatar asked Jan 18 '18 18:01

erin


People also ask

What does async pipe return?

Angular's async pipe is a pipe that subscribes to an Observable or Promise for you and returns the last value that was emitted.

What is the async pipe doing in this example?

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.

How do you test async on Jasmine?

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.

Why async pipe is used?

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.


2 Answers

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

like image 133
vince Avatar answered Oct 25 '22 02:10

vince


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.

like image 34
Pavel Agarkov Avatar answered Oct 25 '22 02:10

Pavel Agarkov