从服务返回的 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 => 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 然后创建一个新的并与服务结果进行比较。
同样,async
和 fakeAsync
都不能与时间相关的可观察操作一起使用,但使用 jasmine-marbles
您可以将测试调度程序注入它们,它会像魅力一样工作并且没有任何超时 - 立即! Here我有一个如何注入测试调度器的例子。
【讨论】:
以上是关于从服务返回的 Observable 的单元测试值(使用异步管道)的主要内容,如果未能解决你的问题,请参考以下文章
如何从使用 RxSwift 返回 Observable 的服务中获取值
在 RxSwift 中,我如何对 observable 未发送任何事件进行单元测试?
Angular 8 karma 从服务中测试 Observable 的成功和错误