ru:https://highload.today/blogs/navishho-rozrobnikam-yunit-testi-na-angular-pokrokovij-gajd-testuvannya-z-prikladami/ ua:https://highload.today/uk/blogs/navishho-rozrobnikam-yunit-testi-na-angular-pokrokovij-gajd-testuvannya-z-prikladami/
logo
Тестування      21/04/2023

Навіщо розробникам юніт-тести на Angular: покроковий гайд тестування з прикладами

Євген Бодня BLOG

Angular Developer в NIX

Написання юніт-тестів декому здається складним і, на перший погляд, взагалі непотрібним. У цій статті я доведу зворотнє. Ми розглянемо приклади юніт-тестів на Angular у форматі live coding. Матеріалу буде багато, тож приготуйтесь повністю поринути в тему.

Для чого потрібні юніт-тести

Юніт-тестування дозволяє перевірити найменші частин коду. Це можуть бути: поле класу, об’єкт, метод, функція тощо. Такі складові кожної програми називають юнітами. Звідси й назва методу тестування.

Хоча інколи фрагменти коду описують як «модулі», а тести відповідно — модульними.

Юніт-тести допомагають переконатися, що окремі частини ПЗ працюють так, як від них очікується. Це певною мірою рятує від багів, адже тестування покаже, як працюватиме код в тих чи інших випадках.

Юніт-тести можуть виявити слабкі сторони застосунку, навіть коли pull request підтверджує, що все ОК. Якщо при зміні коду виникла проблема, ви побачите місце, де щось порушили.

Чим ще допоможе юніт-тестування?

  • Раннє виявлення помилок. Чимало проблем в коді можна виявити ще до передачі продукту QA-інженеру, безпосередньо в ході розробки. Тому ваші юніт-тести можуть попередити певні великі баги і поліпшити подальше тестування.
  • Впевненість у сумісності. При оновленні окремого функціоналу чи ПЗ в цілому можуть виникати проблеми у взаємодії нових фрагментів коду зі старими. Юніт-тести покажуть, що доданий код не зачіпає наявний.
  • Економія часу. Завдяки виявленню помилок на ранньому етапі та глибинному рівні розробники можуть заощадити свій час. В умовах (майже завжди) обмеженого бюджету і довготривалості проєкту це суттєва перевага.

У Test Driving Development та Behavior Driving Development юніт-тести є невід’ємною частиною програмування. У першому випадку розробник ще до написання коду створює для нього тести. Тобто код одразу має відповідати тестам. Підхід BDD передбачає, що код тестується тільки після його написання. Цей принцип на практиці діє частіше.

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

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

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

Чи є тут недоліки? Незначні у порівнянні з перевагами, але все ж таки:

  • Потреба у частій зміні тестів. При модифікаціях функціоналу доводиться щоразу переписувати тести. Це стосується як зміни функції чи її коду, так і додавання нової фічі або рефакторингу. Написані раніше тести втратять актуальність.
  • Тести виявляють не всі баги. Юніт-тестування зосереджено на обмеженій одиниці коду. Тож передбачити всі проблеми неможливо, навіть якщо код добре покритий перевірками. Юніт-тести не покажуть помилок широкого рівня системи, архітектури чи інтеграційних проблем. Ніхто не виключає, що за певних умов чи дій користувача код не спрацює.

Тестуємо код з фреймворком Jasmine

Jasmine — один із кращих для написання юніт-тестів на JavaScript. Його часто використовують Angular-розробники. Jasmine однаково добре підійде і для тестування вже написаного коду, і ще до його створення.

І це не єдині переваги Jasmine.

Інтеграція з Karma за замовчуванням

З використанням Jasmine ця тулза дозволяє запускати тести, які працюють на ноді.

Окрім цього:

  • Angular працює з Karma «з коробки».
  • Karma автоматизує тести для різних браузерів і девайсів.
  • Онлайн-курс "Нотації BPMN" від Laba.
    Опануйте мову BPMN для візуалізації бізнес-процесів, щоб впорядкувати хаос у них.Після курсу ви точно знатимете, що саме обрати для розв’язання завдань вашого бізнесу.
    Дізнатись більше
  • Слідкує за змінами файлів і сама перезапускає тести.
  • Має якісно прописану документацію.
  • Зручний функціонал. Jasmine має вбудовані методи для тестування коду.
  • Легка інтеграція зі звітністю. Ви швидко зрозумієте, де і що в програмі пройшло тести, і де знайдено помилку.

При ініціалізації проєкту на Angular Jasmine підключений за замовчуванням. Хоча можете замінити його на інші бібліотеки та фреймворки.

Переходимо до найцікавішого — розбору прикладів

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

Відкриваємо karma.conf.js. Це конфігурація Karma, і в більшості випадків тут торкатися нічого не потрібно. Можливо, коли проєкт розбивається на мікроархітектуру, тоді вам знадобиться щось змінити. Цей конфіг застосовується для всього проєкту.

Онлайн-курс "Project Manager" від Laba.
Станьте проджектом, що вміє передбачати ризики наперед і доводити проєкт до результату, який хочуть замовники. Поділиться досвідом Павло Харіков, former Head of PMO в Kyivstar.
Програма курсу і реєстрація

За допомогою файлу test.ts, який використовує конфігурацію Karma, відбувається пошук файлів формату spec.ts. Саме тому при створенні компоненти з Angular-командою автоматично створюються spec-файли. Вони потрібні для юніт-тестування.

Також в test.ts ініціалізується ангулярівське тестове середовище, шукаються файли і запускаються тести.

Розглянемо детально архітектуру. Передусім є звичайний компонент app, в якому зараз нічого немає, а в app-модулі імпортується shared-модуль.

Я розгляну тестування shared.module та його елементів.

Занурюватися в deep-testing не будемо — це занадто довго в межах однієї статті. Але навіть описаних далі знань вам вистачить, аби потренуватись і використовувати у своїх проєктах. Ви зможете писати тести і покривати ними потрібні таски.

Основи Python для школярів від Ithillel.
Відкрийте для вашої дитини захопливий світ програмування з нашим онлайн-курсом "Програмування Python для школярів". Ми вивчимо основи програмування на прикладі мови Python, надаючи зрозумілі пояснення та цікаві практичні завдання.
Зареєструватися

Тестування пайпів

Вони потрібні для трансформації даних. У HTML-файлі використовується pipe через вертикальну лінію та його ім’я. Дані проходять через pipe, мов через трубу: заходять у нього і звідти ж виходять.

Базова функція — transform — змінюватиме дані у потрібний нам спосіб.

Стандартний pipe, який повинен існувати в коробці Angular — toUpperCase. Ми хочемо конвертувати текст в UpperCase. Для цього потрібно перевірити, чи є значення рядком та привести його до верхнього регістру, викликавши метод toString toUpperCase. Це і є функціоналом нашого pipe.

Пайпи бувають різні, але всі вони модифікують дані, не торкаючись кореневих даних. Просто повертають їх для відображення.

Тож почнемо з наявного аргументу — value:

В аргументі потрібен рядок, який треба змінити до верхнього регістру:

Онлайн-курс "Проджект-менеджер в ІТ" від Laba.
Навчіться запускати, контролювати й успішно реалізовувати ІТ-проєкти. Пройти весь шлях проєктного управління на реальному кейсі вам допоможе PMD із 19-річним досвідом в ІТ.
Детальніше про курс

Але з бекенду можуть прийти будь-які інші типи даних. Тому зробимо перевірку, щоб не отримати помилку під час виклику методу toUpperCase:

Якщо тип даних не відповідає нашому очікуванню, то повертаємо пустий рядок. Це дуже простий функціонал: перевірка і повернення значення.

Переходимо до тестування. Для групування тестів існує describe(). Йому достатньо задати ім’я для групи тестів, які будуть запускатися. У нас в даному випадку тестується ConvertToUpperCasePipe. Зазвичай назва класу просто копіюється в кореневий describe(). Він приймає два аргументи: description та specDefinitions. В останній частині треба писати потрібні нам тести. Describe не є тестом — це лише як допоміжна «обгортка».

Робота describe доволі проста. При використанні pipe або будь-якого сервісу треба ін’єктити його в конструкторі. Це патерн в Angular, який створює із сервісу інстанс класу і потім використовує його. В тестах такого немає, тому потрібно все робити вручну. Для цього заявимо змінну let, яку в прикладі назвемо pipeUnderTest. Сюди впишемо інстанс класу для тесту:

Також знадобиться функція beforeEach(). Вона потрібна для виконання певних дій в callback, який передається цій функції перед кожним тестом. Наприклад, потрібна ініціалізація об’єкту класу, щоб не виникло мутацій даних, їх збереження чи обробки при виконанні наступних тестів. Перед кожним тестом даємо чистий код, щоб у тестів все було по-новому — і вони могли це тестувати.

Курс-професія "Motion Designer" від Skvot.
Навчіться створювати 2D- та 3D-анімації у софтах After Effects, Cinema 4D та Octane Render. Протягом курсу ви створите 14 моушн-роликів, 2 з яких — для реального клієнта.
Детальніше про курс

Тож підходимо до першого тесту. Це може бути, наприклад, створення інстансу класу. Якщо у вас є конструктор, в якому використовуються інші сервіси чи пайпи, тести не пройдуть. Це як тест-перевірка, для якого потрібна своя функція. describe() виступає як назва групи тестів, beforeEach() потрібен для виконання певних дій перед тестами. А тестом стане функція it(). В ній є expectation: перший параметр для опису очікувань результату тесту. В нашому випадку це: should create an instance of pipe (повинен створити інстанс пайпу).

Далі необхідно якось написати тест. Для цього існує функція expect(), яка буде використовуватись в усіх без виключення кейсах. Тобто ми очікуємо, що те, що передається, буде чимось або буде мати певний стан. В самому expect є власні методи. Наприклад, toBeTruthy(), який під капотом переводить значення в тип boolean і робить перевірку чи система поверне true. В даному випадку саме toBeTruthy() цілком достатньо. Якщо не відбудеться ініціалізація pipeUnderTest, то система поверне undefined, і тест не буде успішно виконано.

Далі можемо запустити тест командою ng test. Завдяки цьому стартують усі наявні тести: spec-файли, які визначені у файлі test.ts за замовчуванням.

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

Але я розбираю усе в IDE WebStorm, де цей функціонал вбудований. Тож достатньо натиснути на кнопку для запуску тесту. При цьому окремо окреслю: expect() і toBeTruthy() є функціоналом Jasmine.

Курс-професія "Junior Data Analyst" від robot_dreams.
Комплексний курc для всіх, хто хоче опанувати нову професію з нуля.На прикладі реальних датасетів ви розберете кожен етап аналізу даних.
Програма курсу і реєстрація

Після цього Karma запускає тест.

Результати можна побачити в браузері. Karma каже: все працює, тест виконано — його підсвічено зеленим. А усе інше система просто пропустила.

Також можна замість toBeTruthy() вказати toBeFalsy і запустити повторно через команду:

Система скаже: ми очікували, що це буде false, але це не так, так як наявність нашого інстансу повертає true.

Відкриваємо в браузері: тест не пройдений. Karma показує: що очікувалось, де саме проблема тощо.

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

Повертаємося до toBeTruthy() і перезапуску. Запуск всіх тестів почнеться з шаблону — незалежно від того, запустили ви їх або ні. Далі в браузері відкривається нова вкладка. Навіть якщо закрити її, вона все одно відкриється. Вважається, що користувач зробив це помилково. Тож треба саме зупиняти Karma через консоль або інтерфейс IDE.

Отже, ми переконалися, що об’єкт створився без помилок. Але треба перевірити, чи правильно трансформуються дані. Спочатку слід підписати заголовок. Для цього в describe() описуємо, що це є метод transform. Стиль написання назви тесту метода залежить від стилю коду на проєкті, але здебільшого прийнято писати # для вказівки, що це є метод і ми тестуємо його з відповідною назвою. А далі треба вказати колбек.

Пам’ятайте: підзаголовок нічого не робить. Завдяки йому ви лише бачите тести, які будуть написані та стосуються саме цієї групи.

Далі пишемо: ми очікуємо, наприклад, правильного значення, яке буде повернено у випадку невалідного типу даних.

Для прикладу протестуємо виконання блоку if. Нам повинен повернутися пустий рядок, якщо аргумент неправильного типу.

Онлайн-курс Бізнес-аналіз. Basic Level від Ithillel.
В ході курсу студенти навчаться техніці збору і аналізу вимог, документуванню та управлінню документацією, управлінню ризиками та змінами, а також навчаться моделювати процеси і прототипуванню.
Приєднатися

За допомогою тестів можна перевірити функцію на різні випадки, на різні вхідні дані, чи може взагалі функція в коді справлятися із цим.

Далі пишемо тест. У нас уже є інстанс класу, і можна звертатися до його методів. Тож використовуємо метод transform() і кажемо, що хочемо, наприклад, передати методу значення 123.

Але ми очікуємо рядок, і юзер передав неправильний тип даних. Тому результат виконання запишемо в нову змінну, наприклад, result.

Ми очікуємо, що result буде пустим рядком, використовуючи метод toEqual(). Це має порівняти їх так, як нам потрібно. toEqual() зіставляє значення незалежно від посилання на пам’ять (а об’єкти в JS зберігаються за посиланнями саме в пам’яті).

Тепер напишемо тест, за яким система повинна конвертувати рядок і привести його до верхнього регістру. Тож пишемо should return a converted string if the argument type is valid.

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

Далі напишемо те саме і звернемося до методу інстансу. Для цього можна, наприклад, передати значення This is test.

Але хорошим тоном є винесення таких значень у змінні:

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

Стартуємо тести. Їх можна запускати окремо, але для прикладу оберемо групу та подивимося, що роблять describe() та it().

У результатах можете побачити групу тестів цього пайпу (я виділив її назву):

Онлайн-курс Pyton від Powercode academy.
Опануйте PYTHON з нуля та майте проект у своєму портфоліо вже через 4 місяця.
Приєднатися

Є метод transform() і його тести. it() позначає тести як підпункти, тому що це і є тестами. Тож тести пройдені, функціонал працює — можна пушити! Після цього залишиться зупинити Karma, адже pipe вже покритий тестами. Але можна дописати в describe() й інші передбачувані варіанти, наприклад, неправильний тип або будь-яку ідею чогось подібного до тестів.

Але я пропоную потренуватися на іншому пайпі — TotalPricePipe. Коли користувач в інтернет-магазині закидує до Кошика декілька товарів, йому треба показати загальну суму покупки. В рядку TotalPrice й буде підрахована вартість обраних товарів.

У моделі даних IProduct є усі необхідні для цього поля: id, назва товару, ціна, кількість тощо.

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

Якщо ж продукти є, то підрахуємо загальну ціну, використовуючи змінні totalPrice і currency.

У результаті отримаємо приватний метод для здійснення калькуляції:

Юнітами тут є метод getTotalPrice() та transform(). Але якщо transform() протестувати легко, то перевірка getTotalPrice() може викликати питання. Неможливо просто так отримати доступ до цього приватного методу в тесті. Через це вважається, що приватні методи не потрібно перевіряти взагалі.

Якщо transform(), тобто місце, де використовується приватний метод, проходить тести, то і приватний метод працює коректно, адже в transform() ми надсилаємо різні значення.

Тож протестуємо його. У нас є той самий describe(), де описано CalculateTotalPricePipe. Спочатку треба створити інстанс за допомогою змінної pipeUnderTest та вказати її тип.

Перед кожним тестом необхідно створювати інстанс, аби заново було чисте тестування, без мутацій даних.

Далі пишемо тест в it() фразу should create an instance of pipe. Також передаємо назву і callback. В колбеці ж очікуємо, що pipeUnderTest буде toBeTruthy. Звісно, це можна реалізувати інакше. Наприклад, можна привести до типу boolean, а потім вказати toBeTrue() чи навіть toBe(true). Та це буде розглянуто трошки нижче.

Переходимо до тесту методу transform(). Для цього описуємо його групу тестів і вказуємо очікування: якщо немає продуктів, треба вивести 0 як загальну суму.

Валюту ми об’явимо в тестах. Якщо вона вже об’явлена, її можна використовувати. Але для прикладу розберемо й це. Ми оголошуємо enum Currency та вказуємо валюти. Це, наприклад, долари США та гривні.

Тепер можна вказати, що повинно повернути 0, наприклад, у гривні. Ви можете вставляти різні змінні в назву теста, і все буде підтягуватися. А далі треба сказати, за яких умов ми повинні отримати 0. Тож пишемо в описі тесту when there are no products.

Пишемо, що змінна totalPrice буде дорівнювати результату трансформації даних при передачі методу transform() пустого масиву і валюти Currency.uah.

Залишається лише вказати очікування, що totalPrice буде дорівнювати 0 гривень.

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

У результаті утворився інстанс і пройшов перший тест методу transform(). За переданих аргументів він повернув те значення, яке ми хотіли від нього побачити.

Ускладнимо задачу: тепер система повинна повернути totalPrice, коли продукти є.

Тут слід вказати причини використання суфіксу UnderTest. По стандарту ви створюватимете компоненти (пайпи, класи, будь-що інше). При тестуванні можна застосовувати інші пайпи, компоненти тощо. Тому для розуміння, що тестується, ставиться саме цей суфікс. Це ніби стиль коду.

Тож звертаємося до методу transform() та передаємо продукти. Але ж ми отримуємо їх з бекенду, чи не так? Та бекенд і не потрібний! Ми використаємо макетнні/фейкові (несправжні) дані: це будуть фейкові продукти, які відповідатимуть інтерфейсу IProduct.

Створюємо масив об’єктів і вказуємо назви продуктів. Наприклад, fake-product-1, fake-product-2 та fake-product-3. Назви можуть бути якими завгодно. Також прописуємо ціни: 10, 20 і 30 відповідно. А валюту передаємо, але вона так само може йти з бекенду.

Далі ці фейкові продукти передаємо як готовий масив. Також треба додати валюту — гривню, наприклад.

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

Для рішення проблеми треба написати мокову функцію, яка буде рахувати суму за всі фейкові продукти. Назвемо її, наприклад, getFakeTotalPrice().

Функція, яка все рахує, готова. Тепер зробимо калькуляцію, яку назвемо fakeTotalPrice. Тож просто кажемо getFakeTotalPrice — і вона зробить це з фейковими продуктами.

Далі кажемо: визначений totalPrice буде дорівнювати тому, що ми хотіли б отримати (тобто fakeTotalPrice і передану валюту).

Зверніть увагу: в коді використано пробіл, бо в частині, де на наступній ілюстрації виділено, також був пробіл.

Виникає ще одне питання: з якими аргументами викликається функція? Функціонал же є взагалі будь-де. Тож ви, наприклад, хочете перевірити, що цій функції були передані саме ці аргументи, при цьому вона була викликана саме з цими аргументами, які ви хочете побачити. Тож пишемо ще один тест. Для цього кажемо: функція повинна бути викликана з аргументом, наприклад, Currency.usd.

Звертаємось до функціоналу Jasmine. У ньому є spyOn() — шпигун, який слідкує за методом, який ми хочемо викликати в майбутньому. В даному випадку він не буде змінювати сам метод. Шпигун лише стежить, що відбувається з методом. Тому ми кажемо, що хочемо слідкувати за інстансом pipeUnderTest та методом transform() в ньому.

Далі викликаємо метод transform() і передаємо йому фейкові продукти і бажану валюту.

А як ми перевіримо, чи метод був викликаний? Шпигун слідкує за ним і повертає об’єкт, в якому записані дані шпигунства. Тож запишемо шпигуна в змінну transformSpy.

У шпигуні є чимало функцій. Наприклад, тут бачимо, що він є інстансом jasmine.Spy.

Шпигун уже слідкує, і ні до якої властивості не треба звертатися. Тож далі кажемо: ми очікуємо, що метод, за яким ми шпигуємо, буде викликаний з відповідними параметрами. Для цієї перевірки, існує метод toHaveBeenCalledWith(). Можна також взяти й toHaveBeenCalled(), аби метод був просто викликаний — і система прослідкує, чи це так. Але треба переконатися, що метод, над яким ми слідкуємо, викликаний з параметрами, наприклад, FAKE_PRODUCTS і Currency.usd.

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

До речі, всі дані на ілюстрації нижче — мокові. Їх можна називати фейком, добавляти мок, суфікс або префікс тощо. Головне: ми не чіпаємо оригінальний код, який тестуємо.

Тести пройдено, і ними все покрито. Тож зупиняємо Karma-сервер:

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

А в другій частині статті я розповім про тестування директив. Тож ще побачимося! 🙂

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

Онлайн-курс "Computer Vision" від robot_dreams.
Застосовуйте Machine Learning / Deep Learning та вчіть нейронні мережі розпізнавати об’єкти на відео. Отримайте необхідні компетенції Computer Vision Engineer.
Дізнатись більше про курс

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

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

Топ текстів

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

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

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