Тестирование Observable в Angular

Тестирование Observable в Angular

От автора: в этой статье я хотел бы поговорить о заблуждении, которое я встречал в других статьях о написании тестов для Angular Observable.

 

Давайте рассмотрим этот базовый пример, с которым мы все знакомы.

@Component({ selector: 'app-todos', template: ` <div *ngFor="let todo of todos" class="todo"> {{todo.id}} </div> `
})
export class TodosComponent implements OnInit { todos = []; constructor(private todosService: TodosService) { } ngOnInit() { this.todosService.get().subscribe(todos => { this.todos = todos; }); } }

Спецификация компонентов todos

У нас есть служба данных, которая использует библиотеку Angular HTTP для возврата observable. Авторы других статей в Интернете предполагают, что для тестирования вышеуказанного компонента мы можем создать службу-заглушку, которая возвращает observable of().

const todosServiceStub = { get() { const todos = [{id: 1}]; return of( todos ); }
}; describe('TodosComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ declarations: [ TodosComponent ], providers: [{provide: TodosService, useValue: todosServiceStub}] }) }); it('should...', () => { fixture.detectChanges(); expect(element.querySelectorAll('.todo').length).toEqual(1); });
});

Спецификация компонентов todos

Вы запускаете приведенный выше код. Тест проходит, и вы счастливы. Это то, что я называю обманом. По умолчанию observable of() является синхронным, поэтому вы в основном делаете асинхронный код синхронным. Давайте продемонстрируем это с помощью небольшого дополнения к нашему коду.

@Component({ selector: 'app-todos', template: ` <div class="loading" *ngIf="loading">Loading...</div> <div *ngFor="let todo of todos" class="todo"> {{todo.id}} </div> `
})
export class TodosComponent implements OnInit { loading: boolean; todos = []; constructor(private todosService: TodosService) { } ngOnInit() { this.loading = true; this.todosService.get().subscribe(todos => { this.todos = todos; this.loading = false; }); } }

Компонент todos

Мы добавили элемент загрузки, который должен быть видимым, когда инициируется запрос, но скрытым при вызове функции подписки (то есть запрос успешно завершен). Давайте проверим это.

const todosServiceStub = { get() { return of( [{id: 1}] ) }
}; describe('TodosComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ declarations: [ TodosComponent ], providers: [{provide: TodosService, useValue: todosServiceStub}] }) }); it('should NOT work', () => { fixture.detectChanges(); // The loading element should be visible expect(element.querySelector('.loading')).not.toBeNull(); fixture.detectChanges(); expect(element.querySelectorAll('.todo').length).toEqual(1); // The loading element should be hidden expect(element.querySelectorAll('.loading').length).toEqual(0); });
});

Спецификация компонентов todos

Как вы, вероятно, поняли, вышеупомянутый тест никогда не пройдет. Вы получите сообщение об ошибке: Ожидаемый null не будет null.

Когда мы запускаем detectChanges(), хук ngOnInit() будет запускаться и выполнять функцию подписки синхронно, делая свойство isLoading всегда ложным. В результате тест всегда не проходит. Вероятно, вы помните старые времена, когда мы писали тесты в AngularJS и делали следующее:

spyOn(todosService,'get').and.returnValue(deferred.promise);

Вышеприведенный код полностью действителен — в отличие от observables, promises всегда асинхронны. Позвольте мне показать вам три способа исправить это.

Использование defer()

Те, кто предпочел бы придерживаться promis, могут вернуть observable defer(), который вернет promis с данными.

/** * Create async observable that emits-once and completes
* after a JS engine turn */
export function fakeAsyncResponse<T>(data: T) { return defer(() => Promise.resolve(data));
} const todosServiceStub = { get() { return fakeAsyncResponse([{id: 1}]); }
}; describe('TodosComponent', () => { it('should...', async(async() => { const todos = element.querySelectorAll('.todo'); const loading = element.querySelector('.loading'); expect(loading).not.toBeNull(); await fixture.whenStable(); fixture.detectChanges(); expect(element.querySelectorAll('.todo').length).toEqual(1); expect(element.querySelectorAll('.loading').length).toEqual(0); }));
});

Спецификация компонентов todos

Мы используем defer() для создания блока, который выполняется только тогда, когда полученный observable подписан.

Использование планировщиков

Планировщики влияют на тайминг выполнения задачи. Вы можете изменить планировщики по умолчанию для некоторых операторов, передав дополнительный аргумент планировщика.

// prevent name collision import {async as _async} from "rxjs/scheduler/async"; const todosServiceStub = { get() { const todos = [{id: 1}]; return of(todos, _async); }
}; it('should work..', fakeAsync(() => { fixture.detectChanges(); expect(element.querySelector('.loading')).not.toBeNull(); expect(element.querySelectorAll('.todo').length).toEqual(0); tick(); fixture.detectChanges(); expect(element.querySelectorAll('.todo').length).toEqual(1); expect(element.querySelectorAll('.loading').length).toEqual(0);
}));

Спецификация компонентов todos

Асинхронный планировщик планирует задачи асинхронно, установив их в очередь цикла событий JavaScript. Этот планировщик лучше всего использовать для задержки задач во времени или при планировании задач с повторением интервалов.

Использование jasmine-marbles

RxJS marble тестирование — отличный способ протестировать как простые, так и сложные сценарии observable. marble тестирование использует marble-язык, чтобы указать потоки и ожидания для observable.

import { cold, getTestScheduler } from 'jasmine-marbles'; const todosServiceStub = { get() { const todos$ = cold('--x|', { x: [{id: 1}] }); return todos$ }
}; describe('TodosComponent', () => { it('should work', () => { const todos = element.querySelectorAll('.todo'); const loading = element.querySelector('.loading'); expect(loading).not.toBeNull(); getTestScheduler().flush(); // flush the observables fixture.detectChanges(); expect(element.querySelectorAll('.todo').length).toEqual(1); expect(element.querySelectorAll('.loading').length).toEqual(0); }); });

Спецификация компонентов todos

Этот тест определяет observable cold , ожидающий два фрейма (-), вводит значение (x) и завершается (|). Во втором аргументе вы можете сопоставить маркер значения (x) с вводимым значением (todos).

Анонс

Я хотел бы воспользоваться этой возможностью, чтобы представить Spectator, новую библиотеку тестирования, которую я выпустил на этой неделе. Spectator помогает вам избавиться от всех забот, связанных с плагинами, что дает вам читаемые, эффективные и быстрые модульные тесты.

Автор: Netanel Basal

Источник: https://netbasal.com/

Редакция: Команда webformyself.