Тестовое задание на Angular. Тестирование сервисов.

Aleksandr Serenko
F.A.F.N.U.R
Published in
3 min readJul 5, 2021

В данной статье поговорим немного о тестировании сервисов в 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

--

--

Aleksandr Serenko
F.A.F.N.U.R

Senior Front-end Developer, Angular evangelist, Nx apologist, NodeJS warlock