ru:https://highload.today/blogs/yunit-testi-na-angular-praktichnij-gajd-z-testuvannya-direktiv/ ua:https://highload.today/uk/blogs/yunit-testi-na-angular-praktichnij-gajd-z-testuvannya-direktiv/
logo
Досвід      05/05/2023

Юніт-тести на Angular: практичний гайд з тестування директив

Євген Бодня BLOG

Angular Developer в NIX

У попередній статті я розглянув приклади написання юніт-тестів для пайпів. Але розробнику часто потрібно перевіряти ще й директиви. Тож у другій частині розберемо приклади юніт-тестів для цих частин коду.

Директиви потрібні для модифікації HTML. Наприклад, вони можуть при hover-ефекті зробити текст червоним, задати background, створити tooltip як підказку тощо. Але директиви ніяк не модифікують сам текст.

Для прикладу звернемося до директиви Highlight (підкреслити, виділити):

У ній є mouseenter. Ми вказуємо йому, що при наведенні мишкою через директиву (тобто не через CSS!) на бекграунді має ставитися колір yellow:

Нижче вказуємо: при mouseleave, коли мишка залишає блок із доданою директивою, все буде навпаки — на бекграунді стане transparent:

Далі ми будемо викликати setBackgroundColor. Для цього тут вказано elementRef, посилання на елемент. Ви можете звертатися до нього, його стилів, сутності тощо. Хоча загалом директиви потрібні для додання нового в дизайн:

Ангулярівська директива, звісно, ще не компонент. Відкриваємо її та бачимо опис, що тестуємо цю директиву:

Психологічний профорієнтаційний тест для IT-фахівців від Ithillel.
Пройдіть психологічний профорієнтаційний тест для IT-фахівців щоб дізнатися ваші сильні сторони, вподобання і інтереси і з'ясувати, яка IT-спеціальність вам підходить.
Пройти тест

Важливо аналізувати, що можна протестувати. Немає сенсу перевіряти, чи викликаються setBackgroundColor і mouseenter. Головне переконатися, що директива працює за призначенням. У цьому допоможе моковий компонент. Пишемо class MockComponent і додаємо декоратор Component, аби зробити цей клас компонентою. Далі передаємо звичайний шаблон template, вказуємо div з будь-яким текстом (наприклад, Test) та поверх нього повісимо директиву AppHighlight:

При наведенні на div мишкою йому має ставитися yellow на background. А коли відведемо мишку, то й колір зникне. Для взаємодії з цим компонентом беремо його інстанс через mockComponent і кажемо: ти будеш мати тип MockComponent.

Тепер звернемося до beforeEach. Тут постає питання: чи потрібно нам створювати інстанс класу ElementRef, який ми inject’уємо в конструкторі директиви? Але в нього теж є щось в конструкторі… Може здатися, що потрібно додати щось в конструктор класу при створенні інстансу директиви. Але це зайве! Краще використати утиліту TestBed, яка дозволяє налаштувати середовище тестування в цьому файлі. Це те саме, що NgModule, як в модулі SharedModule із @NgModule() декоратором. У нього записуються і реєструються компоненти, імпортуються модулі, експортуються сервіси тощо. Все це йде під капотом. Тож і ми зробимо все так само!

У TestBed є метод configureTestingModule, який створить модуль для реєстрації цієї компоненти. Всі властивості, імпорти, декларації, провайди тощо — все ідентичне. Далі ми кажемо, що реєструємо мокову компоненту. Також у модулях завжди треба реєструвати й директиву.

Після запуску коду все повинно працювати, і нам не потрібно створювати ніяких інстансів вручну. Система під капотом залізе в директиву, візьме ElementRef та створить йому інстанс за Dependency Injection патерном. Так загалом Angular і працює.

Курс Digital Marketing від Mate academy.
На курсі ви навчитесь запускати рекламу в кабінтеах Facebook/Instagram та Google. Ви також познайомитися з SEO та Email marketing. Це ті навики які найчастіше просять ІТ компанії від Junior Marketers. А ми вас не лише навчимо, а й працевлаштуємо!
Дізнатися більше про курс

Нам потрібен об’єкт для спілкування з компонентою. Тож створюємо і конфігуруємо ще beforeEach перед кожним новим тестом. Але як нам взяти інстанс MockComponent в утиліті TestBed? На цей випадок є fixture, фікстура. Це інстанс ComponentFixture, де треба вказати generic, а саме фікстурою чого він є. У нашому випадку — фікстурою мокової компоненти.

Наступний крок — дістаємо інстанс компонента зі створеної фікстури — componentInstance.

Fixture — це допоміжний об’єкт, який дозволяє звернутися до інстанса компоненти і нативного елемента. Він дозволяє працювати з шаблоном компоненти та здійснювати detectChanges.

Тому треба перебрати ще й функціонал фікстури. Сама ж утиліта дала все необхідне. Візьмемо, наприклад, мокову компоненту. В ній поки що немає властивостей, але вже можна звертатися до її методів та робити з нею що завгодно.

Хочу підкреслити один нюанс Angular. Ви не задалися питанням, чому наші два beforeEach написані не в одному? Перша причина — чистота коду. Ми відокремлюємо конфігурацію і використання сконфігурованого енвайроменту. Друга — конфігурація в Angular запускається асинхронно. Тобто потрібно, щоб при зверненні до мокової компоненти її інстанс вже був готовий.

Онлайн-курс "Фінансовий аналіз" від Laba.
Навчіться читати фінзвітність так, щоб ухвалювати ефективні бізнес-рішення.Досвідом поділиться експерт, що 20 років займається фінансами і їхньою автоматизацією.
Детальніше про курс

Можна здійснювати конфігурацію нашого енвайроменту асинхронно, використовуючи обгортку waitForAsync або роблячи наш колбек асинхронним. Однак в Angular конфігурація енвайронмента і так здійснюється асинхронно. Детальніше про це розповім згодом.

Мокова компонента є, тому пишемо тест. Ми кажемо в it(): ти повинен показати yellow background color, коли буде тригер mouseenter event. Тобто коли мишка буде наведена на цей HTML-елемент, він має отримати жовтий бекграунд.

Нібито можна звертатися до мокової компоненти, але це не так. Адже у fixture все вже є. Компонента — це як функціональність класу всередині. Краще звернутися до фікстури.

У fixture є властивість debugElement, яка робить знімок під час звернення. Також є різні властивості. В нашому випадку обираємо query, аби зробити запит до згенерованого темплейту, де використано той div. І не треба писати звичайний селектор в запиті! Цей функціонал теж готовий.

Тут допоможе ангулярівська утиліта By. Тож кажемо: хочемо звернутися до неї по відомій директиві і знайти HTML-елемент. У результаті воно звернеться до template по query та візьме потрібні дані.

Результат вибірки запишемо у моковий div, який маємо в темплейті.

Курс Frontend розробки від Mate academy.
Front-end розробник одна з найзатребуваніших професій на IT ринку. У Mate academy ми навчимо вас розробляти візуально привабливі та зручні інтерфейси. Після курсу ви зможете створювати вебсайти і застосунки, що вразять і користувачів, і роботодавців.
Дізнатися більше про курс

Звертаємося до mockDiv для виклику потрібного івенту, тобто mouseenter. Другим параметром йде об’єкт, але він не потрібний нам, бо треба просто мишкою навести на HTML-елемент. Підкреслю: мова не про справжню мишку, а про імітацію з коду. Для цього у фікстурі існує triggerEventHandler.

Далі вказуємо: хочемо, аби div взагалі існував у темплейті, оскільки наша директива не повинна його модифікувати/приховувати. Також можна визвати не тільки toBeTruthy, а й, наприклад, toBeDefined, щоб div взагалі був визначений. Це є аналогом.

Також можна протестувати, чи став бекграунд жовтим після наведення курсору. Для цього звертаємося до div як до DOM’івського елементу через nativeElement та беремо його style і backgroundColor. Бекграунд має бути жовтим, тому додаємо toEqual yellow.

Запускаємо тест…

Результат: все перевірено, жовтий є. До речі, в тести можна додати console.log. Вони будуть відображатися у консолі, якщо потрібно побачити деталі чи вивести змінну.

Онлайн-курс "Чистий код та патерни проєктування" від robot_dreams.
Прискорюйте й спрощуйте процес розробки.Під менторством лектора з 15-річним досвідом ви навчитеся застосовувати 20+ шаблонів, опануєте рефакторинг і принципи чистого коду.
Детальніше

Тепер треба прибрати курсор, аби встановлені стилі не збереглися. Інакше буде баг, а не фіча 🙂 Вказуємо в it(): встановити transparent background color при тригері івенту mouseleave.

Далі обираємо той самий div з HTML за допомогою fixture.

Після цього надаємо йому той самий івент, але вже при mouseleave.

І знову копіюємо expect, але в кольорі заміняємо yellow на transparent.

Із triggerEvent можна робити не тільки прості, стандартні івенти. В компонентах є й свої івенти, які за допомогою декоратора @Output() відправляються зовнішній батьківській компоненті. Їх також можна тестити — для цього достатньо написати в eventName потрібну назву. Тож запустимо тільки цей тест.

Онлайн-курс "Бренд-менеджмент" від Laba.
Розберіться в комплексному управлінні брендом: від його структури до комунікації з аудиторією.Дізнайтесь принципи побудови бренд-стратегії, проведення досліджень і пошуку свого споживача.
Детальніше про курс

Інші тести, які Karma не запускала, виділені сірим кольором. Запущені тести, які успішно виконані, мають зелене забарвлення.

Коли ви розумієте, що можна окремо запускати тести і функціоналу зсередини вистачає для цього — спокійно так робіть. Якщо ж, наприклад, директива впливає на щось інше і потрібний результат, або якщо це форма, яку білдить компонента, — знадобиться інший підхід.

Адже вони впливають ззовні, тому краще робити все через мокові компоненти. Це дозволить конфігурувати їх і не перейматися залежностями. Ви можете використовувати це як звичайний ангулярівський модуль. Якщо це простий pipe, можна створити new Pipe() як інстанс, використати цю компоненту і записати в змінну.

Після того, як все збілдиться, матимете доступ до template, функціоналу компоненти тощо. Тобто візьмете з мокового компонента інстанс, який створив TestBed, — і будете його використовувати. Навіть якщо у вас буде поле, припустимо, aaa = 2, то з’явиться доступ і до нього:

Тобто можна звернутися і до полів, і до методу мокової компоненти.

Розглянемо більш складну директиву — коли стоїть задача змінювати теми блоку на сайті. Наприклад, хтось повісив це на батьківській div, і тема всіх елементів змінилась.

Англійська для IT від Englishdom.
В межах курсу можна освоїти ключові ІТ-теми та почати без проблем говорити з іноземними колегами.
Дійзнайтеся більше

Цього разу ми маємо непросту директиву, бо всередині є @Input(). Зовні треба їй дати значення. Також слід віддати дані на @Output(). Наприклад, ми хочемо поставити директиву, встановити тему Dark та отримувати інформацію про попередню тему. Останнє може знадобитися для аналітики сайту за допомогою платформи Amplitude.

Вона показує, як часто користувач натискав на кнопку або змінював фото профілю. Тож при зміні теми слід надсилати й попередню, яка тривалий час подобалася юзеру.

При встановленні теми використаємо не elementRef, а renderer. Він здійснює кросплатформені зміни і додає елементу клас, який треба зробити з темою. Світлою, темною, дефолтною — будь-якою.

Але є проблема: в директиві тем нічого перевіряти. Усі дані і так прийдуть. Тому просто застосуємо директиву. Припустимо, що ми маємо компоненту MockComponent. Вказуємо декоратор Component, прописуємо template та вішаємо на div зміну теми.

Потрібно змінювати й теми. З цим допоможе button з класом light-btn. По кліку встановлюємо тему, наприклад, Light Theme. Створюємо аналогічну кнопку і для Dark Theme.

Бізнес англійська від Englishdom.
Тут навчають за методикою Кембриджу, завдяки якій англійську вивчили понад 1 мільярд людей. Саме вона використовується в найкращих навчальних закладах світу, і саме за нею створені курси.
Інформація про курс

Далі створюємо метод всередині нього.

Неважливо, що всередині div — це поки що нам е знадобиться. Але ж є @Input(). Тому треба вирішити, яку тему встановити першою при вході користувача на сторінку. Це можна дізнатися з бази даних. Попередньо збережемо цю тему. Для прикладу створимо змінну та назвемо її selectedTheme.

Далі вказуємо тип теми, ThemeType

Також потрібно отримати попередню тему. Тож вказуємо getPreviousTheme та передаємо $event.

А нижче зазначаємо саму тему.

Курс English For Tech: Speaking&Listening від Enlgish4IT.
Після курсу ви зможете найкраще презентувати свої досягнення, обговорювати проекти та вирішувати повсякденні завдання англійською мовою. Отримайте знижку 10% за промокодом TCENG.
Дізнатись про курс

При запуску компоненти отримаємо дані з бекенду, тому необхідно передавати й початкову тему, аби вона взагалі була встановлена. Тож використаємо хук ngOnInit().

Щоб встановити тему потрібно змінити selectedTheme.

Щодо отримання попередньої теми, то можна написати коментар, наприклад, do some actions. Це на майбутнє, для підготовки аналітики.

На цьому все. Надсилаємо першу тему, renderer встановлює клас, юзер обирає в UI для себе Light Theme — і в класі буде зазначено light. Якщо користувач обирає Dark Theme, то так само встановлюється dark, а нам надсилається попередня тема. Тепер варто перевірити функціонал цієї директиви.

Для цього пишемо let mockComponent й обираємо тип MockComponent. Також потрібен fixture, аби звертатися до цього компонента, маніпулювати ним, викликати івенти.

Онлайн-курс Frontend-разробник від Powercode academy.
Курс на якому ти напишеш свій чистий код на JavaScript, попрацюєш із різними видами верстки, а також адаптаціями проектів під будь-які екрани. .
Зарееструватися

Далі прописуємо beforeEach. І тут пропоную цікаву штуку! Пишемо TestBed, додаємо конфігуратор для тестового модулю, а потім декларуємо мокову компоненту і директиву (її слід зареєструвати, аби не було помилки при використанні).

Але це викличе помилку, тому що неможливо скомпілювати компоненту. В декораторі компоненти templateUrl та styleUrls будуть в окремих файлах. Це стандарт в Angular — компоненту треба розбивати на частини. Тому тест не може дістати HTML. Для цього існує метод compileComponents.

Він працює з компонентами, які мають зовнішні файли, як HTML-посилання. Це може бути рядком templateUrl із певним шляхом.

Метод збирає їх та вставляє як template. Так само і стилі:

Після компіляції треба записати це в змінну. Відокремлюємо конфігурацію beforeEach. Вона запускається першою, і це асинхронний код. Інколи в проєктах можна побачити waitForAsync, зачекати для асинхронного виконання. Це береться як обгортка і допомагає beforeEach виконати першим асинхронно. В Angular CLI все виконується під капотом саме асинхронно. А в бібліотеках, фреймворках та інших місцях потрібно додавати саме waitForAsync, щоб користуватися компонентами де завгодно.

Онлайн-курс "Режисура та візуальний сторітелінг" від Skvot.
Перетворюй свої ідеї на сильні історії в рекламі, кліпах чи кіно Досвідом ділиться режисер, продюсер та власник продакшену, який 10+ років у професії.
Детальніше про курс

Заново створюємо мокову компоненту та прописуємо fixture.componentInstance.

І тут згадаємо про наявність в компоненті ngOnInit. При onpush-стратегії наш template не зміниться, навіть якщо в selectedTheme написати масив із мільйоном елементів. Адже він не побачить шаблон. Тому потрібно вручну працювати з changeDetection. Тобто вказати його інстанс, після чого буде викликано OnInit та оновлено template. Прописуємо fixture.detectChanges. Наголошую: не через changeDetectorRef, як в конструкторі. Так запускається механізм виявлення змін. Тобто в тестах це буде трішки не так, як ми звикли.

Переходимо до наступного тесту. Наприклад, вам потрібно, щоб при ініціалізації компоненти дефолтний клас відправився в тему і був встановлений на контейнер. Йдеться про зовнішній клас, написаний вище div. Прописуємо відповідний текст в expectation.

Маємо отримати div та перевірити, чи є взагалі цей клас. Перед кожним тестом ми викликаємо описаний beforeEach і запускаємо detectChanges. Це значить, що ngOnInit() надіслав default, і повинен висіти саме дефолтний клас.

Для перевірки вказуємо const mockDiv, звертаємося до fixture та дебажимо його, а також виконуємо запит по директиві тем через By. Після цього div буде знайдено та записано.

Далі отримаємо класи так само, як звичайний Java-скрипт. Вказуємо mockDiv.nativeElement.classList, який за типом буде DOMTokenList.

У клас треба записати змінну, щоб його перевірити. Нам повернеться масивоподібне типу DOMTokenList. Це не буде масив, адже ми не можемо працювати з ним напряму, як із методами типу include().

Для перевірки цього в expect() задаємо divClasses з методом contains. Він перевіряє, чи елемент включає потрібний клас (тобто default). Ми встановили його від початку, тому його й очікуємо. Для цього бираємо toBeTruthy. Хоча можна й toBeTrue, бо система поверне true або false.

Нарешті запускаємо тест…

І він пройшов перевірку! Коли ми запустили OnInit, в директиву пішов дефолтний клас як @Input(). Директива змогла додати div клас default. А якщо хтось у директиві зробить не в класи, а в стайл атрибути HTML, тест відразу покаже помилку.

Уявімо іншу ситуацію. Припустимо, у нас встановлено клас default. Але перед цим проявився undefined — невизначена змінна. Для тесту прописуємо, що він має дати клас попередньої теми, коли компонента ініціалізована.

Також вказуємо очікування щодо мокової компоненти: аби метод getPreviousTheme спрацював.

Зрозуміти, що він спрацював, допоможе «шпигун». Звертаємося до методів мокової компоненти. Для перевірки створюємо «шпигуна» за допомогою функціоналу Jasmine, у якого є різні методи (наприклад, для асинхронного «шпигуна»).

Але в нашому прикладі для створення «шпигуна» над ним же ми його переписуємо. Тобто ігноруємо функціонал мокової компоненти, який раніше прописали як do some action. Він тут не потрібний, його не треба тестувати. Нам лише варто переконатися, чи взагалі вона викликалась. Щоб createSpy розумів, за чим стежити, передаємо йому ту саму назву методу.

До цього всього ми закинули default, та перед цим не була записана змінна. Тобто вона не визначена (undefined). Тому й повинна бути викликана саме з undefined.

Запускаємо тест…

Усе пройшло успішно, можна зупинити Karma.

А ще можна подивитися, чи взагалі змінюється тема після кліків по кнопкам. Пишемо відповідний expectation:

Нам потрібно отримати цю кнопку з темплейта, тому прописуємо darkThemeBtn. Щоб натиснути на цю кнопку через код, беремо її з HTML за допомогою fixture — і клікаємо на неї. Для цього беремо query, який зробить запит у стилі CSS. Як в CSS, ми робимо вибірку за класом dark-btn. Тож його і використаємо.

Після цього робимо клік по цій кнопці…

Далі можна детектити зміни, які покаже клік. Тут обійдемося без підкапотного стеження detectChanges.

І тоді після кліку по dark-btn тема має змінитися на Dark Theme. Можна копіювати знайомий фрагмент — беремо з HTML той самий div та його div-класи.

А далі зробимо сперевірку: чи складається divClasses зі слова dark? При цьому не обов’язково мати toBeTruthy, тут може бути й toBeTrue.

Вкажемо очікування про надсилання попередньої теми після натискання на кнопку. Для цього звертаємося до компоненти. Залишається вказати toHaveBeenCalledWith — але не з undefined, як вище, а з default, як було від самого початку.

Також можна створити ідентичний тест, але усюди змінити dark на light.

Запускаємо тести і слідкуємо, що буде з директивою…

Результат: директива пройшла всі написані тести!

Підіб’ємо підсумки

Для тестування директиви добре підходить використання мокових компонентів. Це дозволяє перевірити її функціонал загалом. У компоненті можна будь-коли запускати changeDetection, адже він відключається. Також завдяки жасмінівському «шпигуну» можна відстежувати, коли метод був викликаний.

 triggerEvent дозволяє надсилати будь-які івенти click та інші, зокрема кастомні івенти. Можна отримати і викликати цю кнопку з самої компоненти. А за допомогою Spy відстежувати, що відбувалося з потрібним HTML-елементом, з кнопкою, чим вона була викликана, що повернула тощо. Це правильно і для синхронного, і для асинхронного коду. «Шпигун» також дозволяє переписувати функціонал. Наприклад, записати потрібну поведінку всередині функції через callFake().

За цим посиланням можете детальніше ознайомитись з кодом і самостійно пройти всі кроки.

Проведення юніт-тестування на проєкті в значній мірі залежить від ресурсів замовника. Простіше кажучи, від бюджету та наявності часу на цю активність. Однак для розробника вміння тестувати — мастхев-навичка в якісній підтримці коду. Тим паче навчитися тестуванню компонентів і сервісів можна за кілька тижнів. Завдяки цьому ваш код стане надійнішим, а ви будете більш впевненим у своїй роботі.

Додаткові ресурси по темі юніт-тестування

Якщо ви знайшли помилку, будь ласка, виділіть фрагмент тексту та натисніть Ctrl+Enter.

Курс Quality Assurance (QA) від Mate academy.
Курс QA — ідеальний для новачка. Від основ тестування до складних стратегій — опануйте всі технології, щоб жодна помилка не змогла вас оминути. Ми впевнені в якості нашого курсу, тому гарантуємо вам працевлаштування після його завершення.
Зареєструватись на курс

Цей матеріал – не редакційний, це – особиста думка його автора. Редакція може не поділяти цю думку.

Найбільш обговорювані статті

Топ текстів

Ваша жалоба отправлена модератору

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: