从服务返回的 Observable 的单元测试值(使用异步管道)

Posted

技术标签:

【中文标题】从服务返回的 Observable 的单元测试值(使用异步管道)【英文标题】:Unit testing value of Observable returned from service (using async pipe) 【发布时间】:2018-06-27 22:31:37 【问题描述】:

运行 Angular/Jasmine/Karma,我有一个组件使用服务来设置 Observable 'items' 数组的值。我使用异步管道显示它。效果很好。

现在,我正在尝试设置一个单元测试并让它通过,但我不确定我是否正确地验证了 'items' 数组是否获得了正确的值。

这里是相关的组件 .html 和 .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>

还有我的 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));
    );

具体来说,我想知道:

1) 我开始创建一个异步“it”测试,但当它不起作用时我感到很惊讶。当我使用异步数据流时,为什么同步测试有效?

2) 当我检查 component.items 与 Observable.of(mockItems) 的等价性时,我是否真的在测试这些值是否相等?还是我只是测试它们都是 Observables?有没有更好的办法?

【问题讨论】:

您还不需要异步测试,因为您的服务会立即返回一个可观察对象(应该如此 - 真正的值稍后出现)。模板中的异步管道是稍后获得真正价值的地方。所以现在你的测试只是测试代码的同步部分。稍后,如果您测试模板呈现项目的情况,您可能需要进行异步测试。 所以是的,您实际上只是在测试您是否正在返回一个 observable。我的猜测是 expect(component.items).toEqual(Observable.of(mockItems)); 通过,因为即使 Observable.of(mockItems)); 是一个不同的对象引用,然后你的模拟返回的 observable,所有的属性可能是相等的,所以对象被认为是等效的。 感谢@FrankModica。好吧,这就是我害怕的。那么,有没有办法测试 Observable 最终会返回正确的数据呢?或者是测试模板呈现它的唯一解决方案,正如您上面建议的那样?我可以这样做,但似乎它不是孤立地测试。如果我的测试失败,我不知道是组件逻辑还是模板有问题,如果你明白我的意思的话。 您可以通过不使用async 过滤器来避免测试模板。相反,您可以订阅组件中的 observable,然后从那里取出项目。然后你可以四处寻找如何测试异步 observable 的返回值。 我曾考虑过,但我真的很喜欢使用异步管道。它似乎使代码更干净。我想知道我是否可以通过这种方式提取值:component.items.toPromise().then(result =&gt; expect(result).toEqual(mockItems); ); 【参考方案1】:

Angular 提供了用于测试异步值的实用程序。您可以使用async utility with the fixture.whenStable 方法或fakeAsync utility with the tick() function。然后使用DebugElement,您实际上可以查询您的模板以确保正确加载值。

两种测试方法都可以。

async 实用程序与whenStable 一起使用:

保持你的设置不变,你很高兴去那里。您需要添加一些代码来获取列表的调试元素。在你的beforeEach:

const list = fixture.debugElement.query(By.css('list-group'));

然后,您可以深入研究该列表并获取单个项目。我不会过多介绍如何使用DebugElement,因为这超出了这个问题的范围。在此处了解更多信息:https://angular.io/guide/testing#componentfixture-debugelement-and-querybycss。

然后在你的单元测试中:

 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');
           */
        
    ));

fakeAsync 实用程序与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');
        */
));

因此,总而言之,不要为了简化测试而从模板中删除 async 管道。 async 管道是一个很棒的实用程序,可以为您进行大量清理工作,此外,Angular 团队还为这个确切的用例提供了一些非常有用的测试实用程序。希望上述技术之一有效。听起来像是在使用DebugElement,并且上述实用程序之一将帮助您很多:)

【讨论】:

最佳实践是您应该在 ngOnInit() 中获取数据,因此您必须创建一个用于测试的模拟,并且在 describe() 的 beforeEach() 中您必须添加一个 spyOn(component, ' methodToGetData').and.returnValue(of(mock)),捕捉服务调用,并返回一个漂亮的模拟而不是真正的调用。【参考方案2】:

有一个用于测试 observables 的包jasmine-marbles 有了它,你可以像这样编写测试:

...
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$);
    );

但这可能不是最好的例子——我通常使用这个包来测试可观察的链。例如,如果我的服务在一些 .map()ing 中使用输入 observable - 我可以模拟原始 observable 然后创建一个新的并与服务结果进行比较。

同样,asyncfakeAsync 都不能与时间相关的可观察操作一起使用,但使用 jasmine-marbles 您可以将测试调度程序注入它们,它会像魅力一样工作并且没有任何超时 - 立即! Here我有一个如何注入测试调度器的例子。

【讨论】:

以上是关于从服务返回的 Observable 的单元测试值(使用异步管道)的主要内容,如果未能解决你的问题,请参考以下文章

如何在此测试中返回 observable 的下一个值?

如何从使用 RxSwift 返回 Observable 的服务中获取值

在 RxSwift 中,我如何对 observable 未发送任何事件进行单元测试?

Angular 8 karma 从服务中测试 Observable 的成功和错误

如何从订阅另一个函数的observable的函数返回布尔值?

如何将 observable 的值从服务传递到组件以更新模板