Тестовое задание на Angular. Тестирование компонентов.
В данной статье рассмотрим тестирование компонентов, где основная задача это проверить связку 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