在 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 请求返回的 Observables 的问题,这可能是一个错误。这是一个演示上述案例的 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-149288405injectAsync 现在也被弃用了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 返回数据

Angular 2 路由器使用 Observable 解析

如何在模板 Angular 2 中呈现 observables 数组长度

Angular 2 变化检测与 observables