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

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

В данной статье рассмотрим тестирование компонентов, где основная задача это проверить связку HTML + JS.

RoomDetails

Возьмем простой компонент где выводится статичный контент:

Напишем тест:

Начнем с базового. При тестировании связки HTML + JS важно проверить, что все “переменные” корректно подставляются в шаблоне в HTML. Для того, чтобы это проверить, нужно найти нужный HTML элемент и взять его значение.

Так как это очень частая и востребованная задача, создадим класс PageObject, который поможет быстро получать значения или сами элементы по id.

Как можно увидеть из реализации, в HTML ищутся элементы по свойству “automation-id”.

PageObject предоставляет 3 важных метода:

  • getByAutomationId — получение DebugElement’а по ID,
  • getAllByAutomationId — получение всех элементов по селектору,
  • text — получение текста для конкретного элемента.

Так как компонент RoomDetails включает в себя Input/Output создадим Wrapper Component, который пробросит нужные значения в тестируемый компонент:

@Component({
template: `<app-room-details automation-id="room-details" [room]="room"></app-room-details>`,
})
export class WrapperComponent {
room = ROOM_EXTENDED_STUB;
}

Теперь создадим PageObject для RoomDetails:

export class RoomDetailsComponentPo extends PageObject<WrapperComponent> {
get roomDetailsStarText(): string | null {
return this.text(RoomDetailsAutomation.RoomDetailsStar);
}

get roomDetailsPersonText(): string | null {
return this.text(RoomDetailsAutomation.RoomDetailsPerson);
}

get roomDetailsAddressText(): string | null {
return this.text(RoomDetailsAutomation.RoomDetailsAddress);
}
}

Как видно из примера, данный класс позволяет получить 3 свойства из шаблона: roomDetailsStarText, roomDetailsPersonText и roomDetailsAddressText.

Отметим, что такие длинные имена это излишки работы с enterprise. В данном случае переменные можно было бы создать без префикса и привести к виду: starText, personText и addressText.

Из реализации видно, что все ID’s для связи хранятся в enum’е:

enum RoomDetailsAutomation {
RoomDetailsStar = 'room-details-star',
RoomDetailsPerson = 'room-details-person',
RoomDetailsAddress = 'room-details-address',
}

Аналогично предыдущему примеру, можно использовать — {Star = ‘star’, …}

Реализация тестирования аналогична обычному тестированию в Angular.

Сначала опишем переменные для pageObject и fixture:

let pageObject: RoomDetailsComponentPo;
let fixtureWrapper: ComponentFixture<WrapperComponent>;

Затем создаем тестовое окружение:

beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
imports: [MatIconModule],
declarations: [RoomDetailsComponent, WrapperComponent],
}).compileComponents();
})
);

В данном случае используется waitForAsync вместо async/await, чтобы минимизировать затраты на поддержку Angular. Обычно они работают одинаково, за исключением пары кейсов.

После создания окружения, получаем экземпляры:

beforeEach(() => {
fixtureWrapper = TestBed.createComponent(WrapperComponent);
pageObject = new RoomDetailsComponentPo(fixtureWrapper);
});

Важным отличием от базовых тестов состоит в том, что из beforeEach выносится fixture.detectChanges() и выполняется в каждом тесте самостоятельно.

Принцип работы fixture.detectChanges это запуск поиска изменений в компоненте. Другими словами первый detectChanges выполнит роль запуска хука ngInit, остальные же запуски будут аналогичны ngChanges.

Тогда базовый тест на инициализацию примет вид:

it('should create', () => {
fixtureWrapper.detectChanges();

expect(fixtureWrapper.componentInstance).toBeTruthy();
});

А тест на соответствие контента будет следующим:

it('should show details', () => {
fixtureWrapper.detectChanges();

expect(pageObject.roomDetailsStarText).toBe('star 4.99');
expect(pageObject.roomDetailsPersonText).toBe('emoji_events Суперхозяин');
expect(pageObject.roomDetailsAddressText).toBe(ROOM_EXTENDED_STUB.buildingExtended.address);
});

В данном случае, инициализируем компонент и проверяем контент.

Отметим, что данные, которые приходят в Input() были переданы с помощью WrapperComponent, описание которого было выше.

RoomBookingCard

Возьмем чуть более сложный пример, где используются другие компоненты.

Создадим PageObject для данного компонента:

И добавим тестовый класс по аналогии с предыдущим примером:

В данном случае тест почти идентичен предыдущему, за исключением того, что в шаблоне выводятся другие компоненты.

<div automation-id="room-booking-card" class="room-booking-card" *ngIf="room">
<div automation-id="room-booking-price" class="room-booking-price">{{ room.price | number: '1.0-0' }} ₽ / ночь</div>
<app-room-booking-form automation-id="room-booking-form" [room]="room">
<app-room-booking-price automation-id="room-booking-room-price" [room]="room"></app-room-booking-price>
</app-room-booking-form>
</div>

И тест:

it('should show form and price', () => {
fixtureWrapper.detectChanges();

expect(pageObject.roomBookingCard).toBeTruthy();
expect(pageObject.roomBookingForm).toBeTruthy();
expect(pageObject.roomBookingRoomPrice).toBeTruthy();
expect(pageObject.roomBookingPriceText).toBe('2,500 ₽ / ночь');
});

Пример приведен для того, чтобы продемонстрировать логику работы с тестированием. В данном случае мы не тестируем компоненты, которые подключаем в тестируемом компоненте. Тестируется только та функциональность, которая присуща самому компоненту. А все что использует компонент работает правильно и не требует тестирования (если это не связка).

RoomBookingForm

Рассмотрим пример, где помимо компонентов, используются еще сервисы.

Разберем компонент RoomBookingForm:

Создадим pageObject и класс теста:

Для того чтобы замокировать сервисы будет использоваться ts-mocito.

Сначала создаем переменные для сервиса и мокируемого значения:

let bookingServiceMock: BookingService;
let bookingDetails$: ReplaySubject<BookingDetails>;

Затем создаем класс мока:

bookingServiceMock = mock(BookingService);

И мокируемое значение:

bookingDetails$ = new ReplaySubject<BookingDetails>(1);

Добавляем в тестовое окружение Angular замоканный сервис:

providers: [providerOf(BookingService, bookingServiceMock), ...],

providerOf — это синтаксический сахар над созданием провайдера:

И в конце свяжем замоканный сервис и значение:

beforeEach(() => {
when(bookingServiceMock.bookingDetails$)
.thenReturn(bookingDetails$);
});

Теперь можно написать тесты:

it('should set form', () => {
fixtureWrapper.detectChanges();

pageObject.setForm(FORM_STUB);
fixtureWrapper.detectChanges();

verify(bookingServiceMock.setBookingDetails(deepEqual(FORM_STUB))).once();
});

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

Ссылки

Вернуться к оглавлению — Введение.

Следующая статья — Заключение.

Предыдущая статья — Тестирование сервисов.

Все исходники на 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