Тестовое задание на Angular. Тестирование сервисов.
В данной статье поговорим немного о тестировании сервисов в Angular.
RoomBookingPriceService
Протестируем сервис, который рассчитывает стоимость бронирования:
Напишем класс теста:
Так как сервис не использует DI
, просто создадим его инстенс без использования TestBed
.
Проверим некоторые кейсы, которые будут гарантировать корректность работы данного сервиса:
Рассчитаем стоимость для 1 дня:
it('should calc for 1 day with minimal', () => {
expect(service.calculate({ roomExtended: { price: 1 } as any, bookingDetails: BOOKING_DETAILS_STUB })).toEqual({
cleaning: 0.01,
days: 1,
fee: 0.15,
rent: 1,
total: 1.16,
});
});
Рассчитаем стоимость для 6 дней:
it('should calc for 6 days', () => {
const bookingDetails = {
[BookingField.Period]: {
[BookingField.PeriodStart]: '2021-01-01',
[BookingField.PeriodEnd]: '2021-01-07',
},
[BookingField.Guests]: 1,
};
const roomExtended = { price: 1000 } as any;
expect(service.calculate({ roomExtended, bookingDetails })).toEqual({
cleaning: 60,
days: 6,
fee: 900,
rent: 6000,
total: 6960,
});
});
В обоих тестах заранее известен результат и проверяется только, что расчет сервиса выполнит аналогичные вычисления.
LocalStorage
Рассмотрим LocalStorage
:
Напишем unit тесты для данного сервиса:
В данном случае, не используются сторонние вендоры, и тестируются реактивные свойства.
it('should return value', (done) => {
service.setItem(key, value);
service.getItem(key).subscribe((result) => {
expect(result).toBe(value);
done();
});
});
Выше представлен пример асинхронного теста, где проверяется результат подписки, в частности получение значения из localStorage.
RoomService
Теперь рассмотрим пример более сложного теста, где есть инжект сервисов:
Сам тест примет вид:
В данном случае необходимо протестировать реактивные свойства сервиса. Создадим свойства:
let rooms$: ReplaySubject<Room[]>;
let room$: ReplaySubject<Room>;
beforeEach(() => {
roomFacadeMock = mock(RoomFacade);
rooms$ = new ReplaySubject<Room[]>(1);
room$ = new ReplaySubject<Room>(1);
});
Добавим моки для инжектированных сервисов:
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
providers: [RoomService, providerOf(RoomFacade, roomFacadeMock)],
}).compileComponents();
})
);
И свяжем замокированные сервисы и реактивные свойства:
beforeEach(() => {
when(roomFacadeMock.rooms$).thenReturn(rooms$);
when(roomFacadeMock.room$(anything())).thenReturn(room$);
});
И только после этого получим экземпляр класса для тестирования:
beforeEach(() => {
...
service = TestBed.inject(RoomService);
});
Добавим простой тест, где идет просто обертка над инжектированным сервисом для проверки:
rooms$: Observable<Room[]> = this.roomFacade.rooms$.pipe(filter<Room[]>(Boolean));
В данном случае тест будет выглядеть так:
it('should return rooms', () => {
rooms$.next(ROOMS_STUB);
const expected$ = hot('a', {
a: ROOMS_STUB,
});
expect(service.rooms$).toBeObservable(expected$);
});
Сначала устанавливаем значение для замокированного свойства:
rooms$.next(ROOMS_STUB);
Затем, определяем результат:
const expected$ = hot('a', {
a: ROOMS_STUB,
});
Теперь проверяем, что должны получить ожидаемое значение:
expect(service.rooms$).toBeObservable(expected$);
RoomManager
Теперь рассмотрим более сложный пример:
Создадим класс теста:
Протестируем получение свойства roomsExtended$:
roomsExtended$: Observable<RoomExtended[]> =
this.roomFacade.rooms$.pipe(
filter<Room[]>(Boolean),
switchMap((rooms) =>
rooms.length
? combineLatest(
rooms.map((room) =>
this.buildingFacade.building$(room.building).pipe(
filter<any>(Boolean),
switchMap((building: Building) =>
this.personFacade.person$(building.person).pipe(
filter<any>(Boolean),
map((person) => ({
...room,
buildingExtended: {
...building,
personExtended: person,
},
}))
)
)
)
)
)
: of([])
)
);
Как и в предыдущем варианте замокируем все сервисы и свойства:
let roomFacadeMock: RoomFacade;
let rooms$: ReplaySubject<Room[]>;
let room$: ReplaySubject<Room>;
let buildingFacadeMock: BuildingFacade;
let building$: ReplaySubject<Building>;
let personFacadeMock: PersonFacade;
let person$: ReplaySubject<Person>;
beforeEach(() => {
roomFacadeMock = mock(RoomFacade);
rooms$ = new ReplaySubject<Room[]>(1);
room$ = new ReplaySubject<Room>(1);
buildingFacadeMock = mock(BuildingFacade);
building$ = new ReplaySubject<Building>(1);
personFacadeMock = mock(PersonFacade);
person$ = new ReplaySubject<Person>(1);
});
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
providers: [
RoomManager,
providerOf(RoomFacade, roomFacadeMock),
providerOf(BuildingFacade, buildingFacadeMock),
providerOf(PersonFacade, personFacadeMock),
],
}).compileComponents();
})
);
beforeEach(() => {
when(roomFacadeMock.rooms$).thenReturn(rooms$);
when(roomFacadeMock.room$(anything())).thenReturn(room$);
when(buildingFacadeMock.building$(anything())).thenReturn(building$);
when(personFacadeMock.person$(anything())).thenReturn(person$);
service = TestBed.inject(RoomManager);
});
Тест примет вид:
it('should set roomsExtended$', () => {
building$.next(BUILDING_STUB);
rooms$.next(ROOMS_STUB);
person$.next(PERSON_STUB);
const expected$ = hot('a', {
a: [ROOM_EXTENDED_STUB],
});
expect(service.roomsExtended$).toBeObservable(expected$);
});
Для roomsExtended предоставим все необходимые свойства:
person$.next(PERSON_STUB);
building$.next(BUILDING_STUB);
room$.next(ROOM_STUB);
Определим результат:
const expected$ = hot('a', {
a: ROOM_EXTENDED_STUB,
});
Проверим результат:
expect(service.roomExtended$(anything())).toBeObservable(expected$);
BookingService
Теперь рассмотрим тесты, где необходимо проверить вызов методов других сервисов.
Соответственно класс теста:
Как и в предыдущих примерах, замокаем все сервисы и свойства.
Теперь протестируем следующее:
setBookingVariant(bookingVariant: BookingVariant): void {
this.bookingFacade.setBookingVariant(bookingVariant);
}
Тест:
it('should call bookingFacadeMock method setBookingVariant()', () => {
service.setBookingVariant(BOOKING_VARIANT_STUB);
verify(bookingFacadeMock.setBookingVariant(
deepEqual(BOOKING_VARIANT_STUB))
).once();
});
Как видно из теста, verify проверит запуск данной функции, в частности она должна вызваться один раз.
Ссылки
Вернуться к оглавлению — Введение.
Следующая статья — Тестирование компонентов.
Предыдущая статья — Создание страницы в админ панели.
Все исходники на github/fafnur/barinb.
Группа в Medium: https://medium.com/fafnur
Группа в Vkontakte: https://vk.com/fafnur
Группа в Facebook: https://www.facebook.com/groups/fafnur/
Telegram канал: https://t.me/f_a_f_n_u_r
Twitter: https://twitter.com/Fafnur1
LinkedIn: https://www.linkedin.com/in/fafnur