在 Angular 2 中对 observable 进行单元测试
Posted
技术标签:
【中文标题】在 Angular 2 中对 observable 进行单元测试【英文标题】:Unit testing an observable in Angular 2 【发布时间】:2016-04-09 01:25:57 【问题描述】:在 Angular 2 中对返回 Observable 结果的服务进行单元测试的正确方法是什么?假设我们在 CarService 服务类中有一个 getCars 方法:
...
export class CarService
...
getCars():Observable<any>
return this.http.get("http://someurl/cars").map( res => res.json() );
...
如果我尝试按以下方式编写测试,我会收到警告:'SPEC HAS NO EXPECTATIONS':
it('retrieves all the cars', inject( [CarService], ( carService ) =>
carService.getCars().subscribe( result =>
expect(result.length).toBeGreaterThan(0);
);
) );
使用 injectAsync 并没有帮助,因为据我所知,它适用于 Promise
对象。
【问题讨论】:
另一个问题是当 getCars 没有发出任何值(无论出于何种原因)测试通过(因为没有期望检查!) 【参考方案1】:
Angular
(ver. 2+)的正确方法:
it('retrieves all the cars', waitForAsync(inject([CarService], (carService) =>
carService.getCars().subscribe(result => expect(result.length).toBeGreaterThan(0));
));
异步 Observables 与同步 Observables
了解 Observable 可以是同步的也可以是异步的,这一点很重要。
在您的具体示例中,Observable 是异步(它包装了一个 http 调用)。
因此,您必须使用waitForAsync
函数在一个特殊的异步测试区 中执行其主体内的代码。它拦截并跟踪在其主体中创建的所有 Promise,从而可以在完成异步操作时预期测试结果。
但是,如果您的 Observable 是一个同步,例如:
...
export class CarService
...
getCars():Observable<any>
return Observable.of(['car1', 'car2']);
...
您将不需要waitForAsync
函数,您的测试将变得简单
it('retrieves all the cars', inject([CarService], (carService) =>
carService.getCars().subscribe(result => expect(result.length).toBeGreaterThan(0));
);
大理石
在测试 Observables(尤其是 Angular)时要考虑的另一件事是marble testing。
您的示例非常简单,但通常逻辑比仅调用http
服务更复杂,并且测试此逻辑变得令人头疼。
Marbles 使测试变得非常简短、简单和全面(对于测试ngrx effects 特别有用)。
如果你使用Jasmine
,你可以使用jasmine-marbles,对于Jest
,有jest-marbles,但如果你喜欢别的,有rxjs-marbles,它应该与任何测试框架兼容。
Here 是使用弹珠重现和修复竞态条件的一个很好的例子。
Official guide for testing
【讨论】:
所以,我发现如果我这样做,那么稍后指定另一个测试,这取决于测试在订阅中运行的顺序从另一个测试仍然处于活动状态。解决这个问题的正确方法是什么?在 lambda 函数结束时取消订阅? 请注意,从 2.7.0 开始支持 jasmine 中的异步 @PieterDeBie 您正在混合使用async
关键字(从 Jasmine 2.7.0 开始确实支持)和来自 Angular 测试实用程序 (import async from '@angular/core/testing';
) 的 async
函数。在这个特定的例子中是后者。
正如我所说,你说的是async/await
:jasmine.github.io/tutorials/async。这是 javascript 功能,而不是 Jasmine:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…。 Jasmine 2.7 支持它的唯一原因是 2.7 支持 Promises。这里我们讨论的是不同的情况——Observables,Angular 有一个特殊的函数叫做async
,它允许你测试异步的 Observables。
async 作为关键字似乎已经解决了我的特殊情况,但你是完全正确的。【参考方案2】:
https://angular.io/guide/testing 目前显示了几种方法。这是一个:
it('#getObservableValue should return value from observable', (done: DoneFn) => service.getObservableValue().subscribe(value => expect(value).toBe('observable value'); done(); ); );
【讨论】:
即使这种方法有效,但我认为使用 async() 是更好的方法。【参考方案3】:最后,我以一个工作示例结束。 Observable
类有一个 toPromise 方法,可以将 Observable 转换为 Promise 对象。正确的做法应该是:
it('retrieves all the cars', injectAsync( [CarService], ( carService ) =>
return carService.getCars().toPromise().then( (result) =>
expect(result.length).toBeGreaterThan(0);
);
) );
但是,虽然上面的代码适用于任何 Observable 对象,但我仍然遇到从 Http 请求返回的 Observable
s 的问题,这可能是一个错误。这是一个演示上述案例的 plunker:http://plnkr.co/edit/ak2qZH685QzTN6RoK71H?p=preview
更新: 从 beta.14 版本开始,它似乎可以与提供的解决方案一起正常工作。
【讨论】:
我认为这是因为如果在您调用toPromise()
时Observable
序列为空,Promise
将无法解析。 github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/…
我对这个 Promise 和 Observable 的东西没有太多经验,但是如果我将某些内容记录到控制台,我会看到承诺在某个时候解析并记录到控制台。【参考方案4】:
我推荐这种方法,我认为它更优雅:
expectAsync(carService.getCars().toPromise()).toBeResolvedWith(myExpectedValue);
您还可以使用以下方式提供自己的异步匹配器:Jasmine Matcher
【讨论】:
【参考方案5】:AsyncTestCompleter
已弃用 https://github.com/angular/angular/issues/5443。 injectAsync
替换它
https://github.com/angular/angular/issues/4715#issuecomment-149288405但 injectAsync
现在也被弃用了injectAsync
不再被弃用 https://github.com/angular/angular/pull/5721(另见@ErdincGuzel 的评论)
it('retrieves all the cars', injectAsync( [CarService], ( carService ) =>
var c = PromiseWrapper.completer();
carService.getCars().subscribe( result =>
expect(result.length).toBeGreaterThan(0);
c.resolve();
);
return c.promise;
) );
【讨论】:
我在 Angular 存储库中看到了AsyncTestCompleter
的示例,但似乎 AsyncTestCompleter
是 angular 内部的,在公共 API 中找不到。
我更新了我的答案。我没有尝试过,因为我自己不使用 TS。
不幸的是,更新的答案不起作用。 inject 方法不允许返回 Promise 值。顺便说一句,injectAsync 再次被 #5721 弃用【参考方案6】:
我设法让它工作的方式是订阅并在预期之后调用完成。
it('should equal bar', (done: any) =>
bar.getFoo().subscribe(v =>
expect(v).toBe('foo');
done();
);
);
【讨论】:
以上是关于在 Angular 2 中对 observable 进行单元测试的主要内容,如果未能解决你的问题,请参考以下文章
Angular 2 - 使用 Observable 时未加载模型类? [复制]
Angular 2 observable 没有“映射”到模型
Angular 2 - 直接从 Observable 返回数据