Единичное тестирование наблюдаемого в 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
, насколько я мог видеть.
Ответы
Ответ 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;
}) );