Як розпочати модульне тестування коду JavaScript

Ми всі знаємо, що нам слід писати модульні тести. Але важко знати, з чого почати і скільки часу приділяти тестам порівняно з фактичним впровадженням. Отже, з чого почати? І мова йде лише про тестування коду чи модульні тести мають інші переваги?

У цій статті я розповім про різні типи тестів і про те, які переваги приносить модульне тестування командам розробників. Я продемонструю Jest - структуру тестування JavaScript.

Різні види тестування

Перш ніж ми заглибимося в особливості модульного тестування, я хочу швидко пройти різні типи тестів. Навколо них часто буває плутанина, і я не здивований. Іноді межа між ними досить тонка.

Блокові тести

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

export function getAboutUsLink(language){ switch (language.toLowerCase()){ case englishCode.toLowerCase(): return '/about-us'; case spanishCode.toLowerCase(): return '/acerca-de'; } return ''; }

Інтеграційні тести

У якийсь момент ваш код взаємодіє з базою даних, файловою системою або іншою третьою стороною. Це може бути ще один модуль у вашому додатку.

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

Функціональні тести

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

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

Тож тепер давайте трохи детальніше розглянемо модульні тести.

Чому я повинен дошкуляти письмовим тестам?

Щоразу, коли я запитую розробників, чи писали вони тести для своєї програми, вони завжди кажуть мені: "У мене не було часу на них" або "Мені вони не потрібні, я знаю, що це працює".

Тож я ввічливо посміхаюся і кажу їм те, що хочу вам сказати. Одиничні тести стосуються не лише тестування. Вони також допомагають вам іншими способами, тому ви можете:

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

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

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

Прийміть кращі архітектурні рішення. Зміни коду, але деякі рішення щодо платформи, модулів, структури та інших потрібно приймати на ранніх стадіях проекту.

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

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

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

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

Як написати свій перший модульний тест JavaScript

Але повернемося до JavaScript. Ми почнемо з Jest, що є основою тестування JavaScript. Це інструмент, який забезпечує автоматичне тестування модулів, забезпечує охоплення коду та дозволяє легко знущатися над об’єктами. Jest також має розширення для Visual Studio Code, доступне тут.

Існують також інші основи, якщо ви зацікавлені, ви можете перевірити їх у цій статті.

npm i jest --save-dev 

Давайте використаємо згаданий раніше метод getAboutUsLinkяк реалізацію, яку ми хочемо протестувати:

const englishCode = "en-US"; const spanishCode = "es-ES"; function getAboutUsLink(language){ switch (language.toLowerCase()){ case englishCode.toLowerCase(): return '/about-us'; case spanishCode.toLowerCase(): return '/acerca-de'; } return ''; } module.exports = getAboutUsLink; 

Я помістив це у index.jsфайл. Ми можемо писати тести в одному файлі, але гарною практикою є відокремлення модульних тестів у виділений файл.

До загальних моделей імен належать {filename}.test.jsі {filename}.spec.js. Я використав перший index.test.js:

const getAboutUsLink = require("./index"); test("Returns about-us for english language", () => { expect(getAboutUsLink("en-US")).toBe("/about-us"); }); 

По-перше, нам потрібно імпортувати функцію, яку ми хочемо перевірити. Кожен тест визначається як виклик testфункції. Перший параметр - це назва тесту для довідки. Інший - це функція стрілки, де ми викликаємо функцію, яку хочемо протестувати, і вказуємо, який результат ми очікуємо. Я

У цьому випадку ми називаємо getAboutUsLinkфункцію з en-USпараметром мови. Ми очікуємо, що результат буде /about-us.

Тепер ми можемо встановити Jest CLI глобально і запустити тест:

npm i jest-cli -g jest 

Якщо ви бачите помилку, пов’язану з конфігурацією, переконайтеся, що ваш package.jsonфайл присутній. Якщо ви цього не зробите, згенеруйте його за допомогою npm init.

Ви повинні побачити щось подібне:

 PASS ./index.test.js √ Returns about-us for english language (4ms) console.log index.js:15 /about-us Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 2.389s 

Чудова робота! Це був перший простий модульний тест JavaScript від початку до кінця. Якщо ви встановили розширення Visual Studio Code, тести автоматично запускатимуться після збереження файлу. Давайте спробуємо, продовживши тест за допомогою цього рядка:

expect(getAboutUsLink("cs-CZ")).toBe("/o-nas"); 

Щойно ви збережете файл, Jest повідомить вас, що тест не вдався. Це допоможе вам виявити потенційні проблеми ще до внесення змін.

Тестування вдосконалених функціональних можливостей та служб знущань

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

import { englishCode, spanishCode } from './LanguageCodes' 

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

import { UserStore } from './UserStore' function getUserDisplayName(){ const user = UserStore.getUser(userId); return `${user.LastName}, ${user.FirstName}`; } 

Цей метод використовує імпортовані UserStore:

class User { getUser(userId){ // logic to get data from a database } setUser(user){ // logic to store data in a database } } let UserStore = new User(); export { UserStore } 

Для того, щоб правильно протестувати цей метод, нам потрібно знущатися UserStore. Макет - це заміна оригінального об’єкта. Це дозволяє нам відокремити залежності та реальні дані від реалізації тестованого методу так само, як манекени допомагають у краш-тестах автомобілів замість реальних людей.

Якби ми не використовували макет, ми б тестували і цю функцію, і магазин. Це було б тестом інтеграції, і нам, мабуть, довелося б знущатися над використаною базою даних.

Знущання над послугою

To mock objects, you can either provide a mocking function or a manual mock. I will focus on the latter as I have a plain and simple use-case. But feel free to check out other mocking possibilities Jest provides.

jest.mock('./UserStore', () => ({     UserStore: ({         getUser: jest.fn().mockImplementation(arg => ({             FirstName: 'Ondrej',             LastName: 'Polesny'         })), setUser: jest.fn()     }) })); 

First, we need to specify what are we mocking - the ./UserStore module. Next, we need to return the mock that contains all exported objects from that module.

In this sample, it's only the User object named UserStore with the function getUser. But with real implementations, the mock may be much longer. Any functions you don't really care about in the scope of unit testing can be easily mocked with jest.fn().

The unit test for the getUserDisplayName function is similar to the one we created before:

test("Returns display name", () => {     expect(getUserDisplayName(1)).toBe("Polesny, Ondrej"); }) 

As soon as I save the file, Jest tells me I have 2 passing tests. If you're executing tests manually, do so now and make sure you see the same result.

Code Coverage Report

Now that we know how to test JavaScript code, it's good to cover as much code as possible with tests. And that is hard to do. In the end, we're just people. We want to get our tasks done and unit tests usually yield an unwanted workload that we tend to overlook. Code coverage is a tool that helps us fight that.

Code coverage will tell you how big a portion of your code is covered by unit tests. Take for example my first unit test checking the getAboutUsLink function:

test("Returns about-us for english language", () => {    expect(getAboutUsLink("en-US")).toBe("/about-us"); }); 

It checks the English link, but the Spanish version stays untested. The code coverage is 50%. The other unit test is checking the getDisplayName function thoroughly and its code coverage is 100%. Together, the total code coverage is 67%. We had 3 use cases to test, but our tests only cover 2 of them.

To see the code coverage report, type the following command into the terminal:

jest --coverage 

Or, if you're using Visual Studio Code with the Jest extension, you can run the command (CTRL+SHIFT+P) Jest: Toggle Coverage Overlay. It will show you right in the implementation which lines of code are not covered with tests.

By running the coverage check, Jest will also create an HTML report. Find it in your project folder under coverage/lcov-report/index.html.

Now, I don't have to mention that you should strive for 100% code coverage, right? :-)

Summary

In this article, I showed you how to start with unit testing in JavaScript. While it's nice to have your code coverage shine at 100% in the report, in reality, it's not always possible to (meaningfully) get there. The goal is to let unit tests help you maintain your code and ensure it always works as intended. They enable you to:

  • clearly define implementation requirements,
  • better design your code and separate concerns,
  • discover issues you may introduce with your newer commits,
  • and give you confidence that your code works.

The best place to start is the Getting started page in the Jest documentation so you can try out these practices for yourself.

Do you have your own experience with testing code? I'd love to hear it, let me know on Twitter or join one of my Twitch streams.