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 передбачає, що код тестується тільки після його написання. Цей принцип на практиці діє частіше.

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

Онлайн-курс "PR Basis" від Skvot.
Дізнайся нюанси різних сфер і обрери свою.Як результат — матимеш стратегію бренду у своєму портфоліо та зможеш стартувати в піарі. Інсайтами ділиться лекторка, яка має 9+ років досвіду.
Детальніше про курс

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

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

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

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

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

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

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

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

Окрім цього:

  • Angular працює з Karma «з коробки».
  • Karma автоматизує тести для різних браузерів і девайсів.
  • Курс English For IT: Communication від Enlgish4IT.
    Почни легко працювати та спілкуватися з мультикультурними командами та міжнародними клієнтами. Отримайте знижку 10% за промокодом ITCENG.
    Інформація про курс
  • Слідкує за змінами файлів і сама перезапускає тести.
  • Має якісно прописану документацію.
  • Зручний функціонал. Jasmine має вбудовані методи для тестування коду.
  • Легка інтеграція зі звітністю. Ви швидко зрозумієте, де і що в програмі пройшло тести, і де знайдено помилку.

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

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

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

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

Онлайн-курс "Тестування API" від robot_dreams.
Навчіться працювати з API на просунутому рівні та проводити навантажувальні тестування, щоб виявляти потенційні проблеми на ранніх етапах розробки.
Програма курсу і реєстрація

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

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

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

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

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

Онлайн-курс "Предметний дизайн" від Skvot.
Навчіться створювати функціональні, трендові та ергономічні дизайни меблів та предметів інтер’єру.
Детальніше про програму курсу і лекторів

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

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

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

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

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

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

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

Курс Full-stack розробки від Mate academy.
Станьте Full-stack розробником з нуля. Mate academy дає комплексні знання і навички для розробки повноцінних веб-рішень — від візуальної частини до серверної логіки. Ви освоїте технології, щоб створити власний проєкт від а до я — без допомоги інших.
Ознайомитися з курсом

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

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

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

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

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

Курс UI/UX дизайну від Mate academy.
На курсі ви навчитесь створювати інтуїтивно зрозумілі та привабливі інтерфейси вебсайтів і застосунків. Ви також освоїте ключові принципи дизайну та дізнаєтесь як виділятися на ринку. А ми вас не лише навчимо, а й працевлаштуємо. Сертифікат теж буде!
Дізнатися більше про курс

Тож підходимо до першого тесту. Це може бути, наприклад, створення інстансу класу. Якщо у вас є конструктор, в якому використовуються інші сервіси чи пайпи, тести не пройдуть. Це як тест-перевірка, для якого потрібна своя функція. 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.

Онлайн-курс "Управління ІТ-командами" від Laba.
Прокачайте свої soft- і hard-скіли в управлінні кількома IT-командами, отримайте практичні стратегії та інструменти ефективного team-ліда.
Програма курсу і реєстрація

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

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

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

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

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

Онлайн-курс "Корпоративна культура" від Laba.
Як з нуля побудувати стабільну корпоративну культуру, систему внутрішньої комунікації та бренд роботодавця, з якими ви підвищите продуктивність команди, — пояснить HR-директор Work.ua.
Детальніше про курс

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

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

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

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

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

Онлайн-курс "React Native Developer" від robot_dreams.
Опануйте кросплатформну розробку на React Native та навчіться створювати повноцінні застосунки для iOS та Android.
Програма курсу і реєстрація

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

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

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

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

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

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

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

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

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

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

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

Онлайн-курс "Продуктова аналітика" від Laba.
Станьте універсальним аналітиком, опанувавши 20+ інструментів для роботи з будь-яким продуктом.
Дізнатись більше про курс

Є метод 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.

Онлайн курс з промт інжинірингу та ефективної роботи з ШІ від Powercode academy.
Курс-інтенсив для отримання навичок роботи з ChatGPT та іншими інструментами ШІ для професійних та особистих задач, котрі допоможуть як новачку, так і професіоналу.
Записатися на курс

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

Топ-5 найпопулярніших блогерів березня

PHP Developer в ScrumLaunch
Всего просмотровВсього переглядів
2434
#1
Всего просмотровВсього переглядів
2434
Founder at Shallwe, Python Software Engineer (Django/React)
Всего просмотровВсього переглядів
113
#2
Всего просмотровВсього переглядів
113
Career Consultant в GoIT
Всего просмотровВсього переглядів
95
#3
Всего просмотровВсього переглядів
95
CEO & Founder в Trustee
Всего просмотровВсього переглядів
94
#4
Всего просмотровВсього переглядів
94
Рейтинг блогерів
Всі публікації автора

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

Топ текстів

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

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

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