Подтвердить что ты не робот

Единичное тестирование наблюдаемого в Angular 2

Каков правильный способ модульного тестирования службы, возвращающей результат Observable в Angular 2? Скажем, у нас есть метод getCars в сервисном классе CarService:

...
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, насколько я мог видеть.

4b9b3361

Ответ 1

Наконец, я заканчиваю рабочим примером. Observable класс имеет метод toPromise, который преобразует объект Observable to Promise. Правильный способ:

it('retrieves all the cars', injectAsync( [CarService], ( carService ) => {
  return carService.getCars().toPromise().then( (result) => {         
     expect(result.length).toBeGreaterThan(0);
  } );       
}) );

Но пока код выше работает с любым объектом Observable, у меня все еще есть проблема с Observable, возвращаемым из запросов Http, что, вероятно, является ошибкой. Вот плункер, демонстрирующий вышеприведенный случай: http://plnkr.co/edit/ak2qZH685QzTN6RoK71H?p=preview

Update:
Начиная с версии beta.14, она работает правильно с предоставленным решением.

Ответ 2

Правильный способ для Angular (вер. 2 +):

it('retrieves all the cars', async(inject( [CarService], ( carService ) => {
     carService.getCars().subscribe(result => expect(result.length).toBeGreaterThan(0)); 
}));

Async Observables vs Sync Observables

Важно понимать, что Observables может быть синхронным или асинхронным.

В вашем конкретном примере Observable является асинхронным (он завершает HTTP-вызов).
Поэтому вы должны использовать функцию async, которая выполняет код внутри своего тела в специальной тестовой зоне асинхронного тестирования. Он перехватывает и отслеживает все promises, созданные в его теле, что позволяет ожидать результатов теста после завершения асинхронного действия.

Однако, если ваш Observable был синхронным, например:

...
export class CarService{
    ...
    getCars():Observable<any>{
        return Observable.of(['car1', 'car2']);
    }
    ...

вам не понадобилась бы функция async, и ваш тест стал бы просто

it('retrieves all the cars', inject( [CarService], ( carService ) => {
     carService.getCars().subscribe(result => expect(result.length).toBeGreaterThan(0)); 
});

Marbles

Еще одна вещь, которую следует учитывать при тестировании Observables в целом и Angular, в частности, мраморное тестирование.

Ваш пример довольно прост, но обычно логика сложнее, чем просто вызов службы http и тестирование этой логики становится головной болью.
Мрамор делает тест очень коротким, простым и всеобъемлющим (особенно полезно для тестирования ngrx effects).

Если вы используете Jasmine, вы можете использовать jasmine-marbles, но если вы предпочитаете что-то другое (например, Jest), есть rxjs-marbles, который должен быть совместим с любой тестовой средой.

Здесь - отличный пример для воспроизведения и исправления состояния гонки с помощью мрамора.


Официальное руководство по тестированию

Ответ 3

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;      
}) );