Модульне тестування (англ. Unit testing) — це метод тестування програмного забезпечення, який полягає в окремому тестуванні кожного модуля коду програми. Модулем називають найменшу частину програми, яка може бути протестованою. У процедурному програмуванні модулем вважають окрему функцію або процедуру. В об'єктно-орієнтованому програмуванні — метод[джерело?].
Модульні тести, або unit-тести, розробляють в процесі розробки програмісти та, іноді, тестувальники білої скриньки (white-box testers).
Зазвичай unit-тести застосовують для того, щоб упевнитися, що код відповідає вимогам архітектури та має очікувану поведінку.
Переваги
Метою модульного тестування є ізоляція кожної частини програми та впевненість у тому, що кожна окрема частина є коректною[1]. Модульний тест забезпечує жорсткий «контракт», за яким має працювати тестований код. Як результат, це надає деякі переваги. Модульне тестування допомагає знайти помилки раніше в циклі розробки ПЗ, що робить розробку дешевшою та швидшою.
Легкий рефакторинг
Модульне тестування дозволяє програмісту, коли він буде змінювати код (проводити рефакторинг) бути впевненим, що модуль працює вірно (це — регресивне тестування). Оскільки модульне тестування вимагає написання тестів для всіх функцій та методів у програмі, помилки швидко локалізуються та виправляються.
Спрощене інтеграційне тестування
Модульне тестування може бути застосоване в інтеграційному тестуванні: тестування окремих модулів та сукупності цих модулів робить інтеграційне тестування легшим. Однак модульне тестування знизу вгору не є інтеграційним тестуванням. Інтеграція з зовнішніми модулями має включатися до інтеграційних тестів, а не до модульних.
Документування
Модульні тести являють собою специфічний вид документації до системи. Розробники можуть подивитися на модульний тест, щоб дізнатися про функції, що виконує модуль, та як його застосовувати.
Unit-тест перевіряє критичні характеристики модулю. Відповідність чи невідповідність цим характеристикам демонструє коректність модуля. Модульний тест «документує» ці критичні характеристики, але не треба покладатися лише на код в документуванні ПЗ під час розробки.
Слід відзначити, що звичайна письмова документація дуже повільно реагує на зміни в коді, тоді як модульні тести завжди відображають поточний стан модуля.
Розробка
Під час розробки програмного забезпечення методом TDD (Test-driven development), модульний тест стає частиною розробки ПЗ. Кожен тест визначає потрібні класи та методи, їх очікувану поведінку. Наведений нижче приклад на мові Java добре демонструє використання unit-тестів під час розробки методом TDD.
Тест-клас за назвою TestAdder визначає декілька програмних елементів, що повинні бути реалізовані. По-перше, у програмі має бути інтерфейс з назвою Adder та клас з назвою AdderImpl, що його реалізує, також має бути конструктор без параметрів. Інтерфейс Adder повинен мати метод add, який приймає 2 параметри (цілі числа) і повертає назад також ціле число. Також цей код визначає поведінку методу add на невеликому наборі значень.
public class TestAdder {
public void testSum() {
Adder adder = new AdderImpl();
assert(adder.add(1, 1) == 2);
assert(adder.add(1, 2) == 3);
assert(adder.add(2, 2) == 4);
assert(adder.add(0, 0) == 0);
assert(adder.add(-1, -2) == -3);
assert(adder.add(-1, 1) == 0);
assert(adder.add(1234, 988) == 2222);
}
}
У цьому випадку, спочатку був написаний модульний тест, а потім вже функціональність, яку він визначає. Цей тест визначає лише поведінку майбутнього методу, залишаючи технічні деталі реалізації програмісту.
Ось метод, що відповідає вимогам, які поставлено тестом:
interface Adder {
int add(int a, int b);
}
class AdderImpl implements Adder {
int add(int a, int b) {
return a + b;
}
}
Важливою перевагою модульного тестування є те, що тести одразу демонструють, відповідає, чи не відповідає реалізація вимогам розробки. Реалізація з помилками, скоріше за все, не зможе пройти модульні тести.
Відокремлення інтерфейсу від реалізації
Призначення модульного тесту — тестування єдиного програмного модулю. Але часто розробники та тестувальники створюють тести, що не відповідають цій умові. У випадку, якщо клас А містить посилання на клас В, тестування класу А перетікає в тестування класу В. Розглянемо приклад.
Є клас, що залежить від бази даних. Модульні тести, які пишуться для нього, можуть взаємодіяти з БД, щоб визначити коректність поведінки класу. Такий підхід є некоректним. У такому випадку модульний тест виходить за межі своєї відповідальності, за межі тестованого класу та перетинає межу іншого класу/процесу/комп'ютерної мережі. Модульні тести, що написані таким чином, перетворюються на інтеграційні тести. Коли ці тести перестають працювати, важко локалізувати помилку.
Правильним підходом можна вважати створення абстрактних інтерфейсів для запитів до БД, а реалізуються ці інтерфейси за допомогою mock-об'єктів (фіктивних об'єктів).
Взаємодія unit-тестів лише з абстрактними інтерфейсами забезпечує ретельніше тестування. Як результат — легше та дешевше супроводження.
Обмеження
Тестування програмного забезпечення не може знайти всіх помилок у програмі. У більшості програм неможливо прорахувати кожен варіант виконання. Це також вірно для модульного тестування. Крім того, модульне тестування, власне, повинне тестувати тільки модулі. Так що цей вид тестування не зможе знайти інтеграційні помилки та інші: помилки архітектури, проблеми з витримкою навантажень на ПЗ. Unit-тестування має проводитись разом з іншими видами тестування програмного забезпечення.
Як і будь-який вид тестування, модульне тестування може визначити лише наявність помилок, а не їх відсутність.
Тестування ПЗ — це комбінаторна задача. Наприклад, кожне логічне судження повинно мати не менш двох тестів: один перевіряє результат істинно, а другий хибно. Внаслідок цього, часто на кожний рядок коду доводиться написати від 3 до 5 рядків тесту[2]. Це вимагає багато часу та грошей, які часто не варті результату.
Застосування
Екстремальне програмування
Модульне тестування — це наріжний камінь екстремального програмування, яке покладається на автоматичне створення тестів. Автоматична генерація тестів може здійснюватись сторонніми бібліотеками, як то jUnit, або власними продуктами компанії-розробника.
Екстремальне програмування використовує створення модульних тестів для test-driven development. Розробники пишуть модульні тести, які визначають усі вимоги до програми. Ці тести будуть неуспішними, якщо потрібні вимоги ще не реалізовано, або якщо реалізація має дефекти.
Екстремальне програмування дотримується стратегії «тестуйте все, що потенційно може викликати помилку», замість традиційної «тестуйте усі можливі шляхи виконання». Тому більша частина коду покрита unit-тестами, але не весь код.
Важливо відзначити, що код тестів вважається одним з найважливіших артефактів проекту, бо супроводжується так само якісно і в такому ж обсязі, як і функціональний код. Розробники відправляють код тестів до репозиторію разом із тим кодом, що вони тестують. Ретельне тестування в екстремальному програмуванні дозволяє легше супроводжувати код, проводити його рефакторинг, інтеграцію, вести добру документацію. Ці модульні тести також працюють як форма регресивного тестування.
Технологія
Модульне тестування зазвичай автоматизують, але його можна проводити й вручну. IEEE не надає перевагу якомусь з цих методів[3].
Мета модульного тестування — ізолювати модуль та визначити його коректність. Автоматизоване тестування дозволяє зробити це ефективно та має багато переваг. Якщо тестування було погано заплановано, то необережне ручне тестування може стати ще й інтеграційним тестуванням, що погано.
Щоб повністю реалізувати ефект ізоляції модуля під час автоматизованого тестування, його код виконують у межах особливого фреймворку, без зв'язку з природним середовищем. Іншими словами, код запускають поза продуктом, або контекстом виклику, для якого його було написано. Такий спосіб тестування показує неважливі залежності між кодом, що тестується та іншими модулями системи.
Використовуючи фреймворк для модульного тестування, розробник задає критерії, за якими визначається успішність тесту. Під час виконання тестування, фреймворк повідомляє про тести, що не задовольняють критеріям. Залежно від важливості невиконаного тесту, фреймворк може зупинити подальше тестування.
Як наслідок, модульне тестування традиційно мотивує програмістів писати код із меншою зв'язністю англ. Coupling, та більшою пов'язаністю англ. Cohesion.
Фреймворки
Фреймворки для модульного тестування зазвичай не розповсюджуються в комплекті з компіляторами — це програмне забезпечення пишуть окремо. Вони розроблені для багатьох мов програмування й допомагають спростити процес тестування. Серед відомих фреймворків є проекти open source, наприклад ті, що зазвичай називають xUnit, та комерційні рішення, такі як TBrun, Testwell CTA++ and VectorCAST/C++.
Також можливо виконувати модульне тестування без додаткових бібліотек, створюючи код, що застосовує механізми assertion, виняткових ситуацій англ. exception, та інші механізми контролю виконання програми, які можуть сигналізувати про неуспіх.
Великий перелік різних фреймворків та їх характеристик наразі доступний в англомовному розділі Вікіпедії: List of unit testing frameworks[en].
Підтримка модульного тестування на рівні мови
Деякі мови програмування підтримують модульне тестування безпосередньо. Їх граматика дозволяє використання модульного тестування без імпорту додаткових бібліотек.
Мови, що підтримують модульне тестування:
Приклад мовою D:
class ABC
{
this() { val = 2; }
private int val;
public func() { val *= 2; }
}
unittest
{
ABC a;
a.func();
assert( a.val > 0 && a.val < 555 ); // Можна звернутися до закритої змінної всередині модуля
}
Див. також
Примітки
Посилання