Unit tests (модульні тести) — це автоматизовані тести, які використовуються для перевірки окремих частин коду, зазвичай однієї функції, методу або модуля. Це один із основних інструментів у практиці розробки програмного забезпечення, який допомагає забезпечити якість, надійність і передбачуваність вашого коду. У цій статті розглянемо, чому варто використовувати модульні тести, які переваги вони дають та як вони впливають на процес розробки.
Правильно написані Unit тести дозволяють виявити помилки на ранніх етапах розробки, покращують якість коду і спрощують його супровід. У цій статті ми розглянемо основи написання Unit тестів для Java-додатку та найкращі практики.
1. Забезпечення якості коду
Unit-тести дозволяють перевіряти точність роботи кожного блоку коду окремо. Завдяки цьому можна виявити баги ще на ранніх стадіях розробки. Якщо код не проходить тестування, розробник одразу отримує сигнал, що щось працює не так, і має змогу виправити ситуацію до того, як продукт “вийде у світ”.
2. Швидке виявлення помилок
Код у програмному забезпеченні може бути складним і мати багато залежностей. Unit-тести дозволяють перевіряти ізольовані частини коду без потреби запускати цілу програму. Це робить пошук і виправлення помилок швидшим і зручнішим.
3. Документування функціональності
Тести виконують також роль документації. Вони показують розробнику (наприклад, новому члену команди), як повинен працювати конкретний код. Просто подивившись на тест-кейси, можна зрозуміти, якими є очікувані вхідні дані, логіка роботи і вихідні результати для певного модуля або функції.
4. Полегшення рефакторингу
Рефакторинг — це процес поліпшення існуючого програмного коду без зміни його зовнішньої поведінки. Unit-тести забезпечують захист під час внесення змін: розробник може бути впевненим, що після рефакторингу програма працюватиме так само добре, якщо тести проходять успішно.
5. Зменшення ризиків при змінах
Якщо ви додаєте нові фічі чи змінюєте наявну функціональність, є ризик випадково “зламати” щось у коді. Unit-тести допомагають швидко дізнатися, чи спричинили зміни небажані побічні ефекти.
6. Економія часу у довгостроковій перспективі
На перший погляд здається, що написання unit-тестів забирає багато часу. Але насправді це скорочує час, який витрачається на пошук багів і їх виправлення в майбутньому. До того ж тести можна використовувати знову й знову, що робить їх інвестицією у стабільність проєкту.
7. Підтримка принципів TDD (Test-Driven Development)
Test-Driven Development (розробка через тестування) — це підхід, за якого тести пишуться перед написанням основного коду. Unit-тести є основою цієї методології. Вони дозволяють концентруватися на вимогах до функціональності перед тим, як приступити до реалізації.
Поширені міфи про unit-тести
- “Це забирає занадто багато часу”
Насправді якісні тести економлять час у майбутньому, оскільки швидше знаходять дефекти. - “Мій код і так працює ідеально”
Жодний код не є ідеальним. Людський фактор, складність алгоритмів і зовнішні залежності майже завжди призводять до помилок. - “Тести не потрібні для невеликих проєктів”
Навіть у маленьких проєктах помилки можуть спричиняти значні витрати. Тести можна почати використовувати поступово навіть в умовах невеликих ресурсів.
Переваги Unit тестування:
- Зменшення кількості помилок у продукті.
- Допомога у виявленні багів на початкових етапах розробки.
- Код стає більш стабільним і передбачуваним.
- Допомагає розробникам упевнено вносити зміни в проект (рефакторинг, додавання нових фіч).
Для Java найчастіше використовують бібліотеку JUnit (найпопулярніша з версій — JUnit 5), а також бібліотеки mock-об’єктів, наприклад Mockito.
Для того щоб почати писати Unit тести на Java, виконайте такі кроки:
1. Додайте JUnit та Mockito до проєкту
Ви можете додати JUnit і Mockito через Maven або Gradle.
Maven: Додайте залежності у ваш файл pom.xml:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>XMLGradle: Додайте в build.gradle:
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
testImplementation 'org.mockito:mockito-core:5.5.0'
}Groovy2. Створіть структуру тестового проєкту
Зазвичай, Unit тести зберігаються у папці src/test/java, окремо від основного коду (src/main/java).
Для написання Unit тестів візьмемо простий приклад. Припустимо, у нас є клас Calculator, який виконує базові арифметичні операції.
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a, int b) {
return a * b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Division by zero is not allowed");
}
return a / b;
}
}JavaНаписання Unit Test
- Створіть тестовий клас Тестовий клас називається так само, як і тестований клас, з додаванням суфікса
Test. ДляCalculatorстворимоCalculatorTest. - Додайте тестові методи Тестові методи мають:
- Починатись з анотації
@Test. - Містити асерти (наприклад,
assertEqualsдля порівняння очікуваного і реального результату).
- Пишіть коректні сценарії Ми протестуємо як “щасливі шляхи” (коли все працює правильно), так і негативні сценарії (наприклад, передача некоректних даних).
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CalculatorTest {
private final Calculator calculator = new Calculator();
@Test
public void testAdd() {
assertEquals(5, calculator.add(2, 3), "2 + 3 should be 5");
}
@Test
public void testSubtract() {
assertEquals(1, calculator.subtract(3, 2), "3 - 2 should be 1");
}
@Test
public void testMultiply() {
assertEquals(6, calculator.multiply(2, 3), "2 * 3 should be 6");
}
@Test
public void testDivide() {
assertEquals(2, calculator.divide(6, 3), "6 / 3 should be 2");
}
@Test
public void testDivideByZero() {
Exception exception = assertThrows(IllegalArgumentException.class, () -> calculator.divide(6, 0));
assertEquals("Division by zero is not allowed", exception.getMessage());
}
}JavaОгляд тестового класу
- Ми перевіряємо поведінку кожного методу окремо.
- Використовуємо анотацію
@Testдля маркування методів як тестових. assertEqualsпорівнює очікуваний результат із реальним.assertThrowsперевіряє, що викликається очікуване виключення.
Робота з Mock-об’єктами через Mockito
Часто доводиться тестувати методи, які взаємодіють із базами даних, сторонніми API чи іншими залежностями. У таких випадках важко використовувати реальну залежність. Тут стають у пригоді mock-об’єкти.
Що таке Mock?
Mock — це об’єкт, який імітує поведінку реальної залежності. Бібліотека Mockito дозволяє створювати такі об’єкти.
public class UserService {
private UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
public String getUserNameById(int id) {
User user = repository.findById(id);
if (user == null) {
throw new IllegalArgumentException("User not found");
}
return user.getName();
}
}JavaСтворимо Репозиторій:
public interface UserRepository {
User findById(int id);
}JavaТестування UserService:
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
public class UserServiceTest {
@Test
public void testGetUserNameById() {
// Створення mock-об'єкта для UserRepository
UserRepository repositoryMock = Mockito.mock(UserRepository.class);
// Налаштування mock для повернення даних
User mockUser = new User(1, "John");
when(repositoryMock.findById(1)).thenReturn(mockUser);
// Створення UserService з mock-репозиторієм
UserService userService = new UserService(repositoryMock);
// Виконання тесту
String result = userService.getUserNameById(1);
assertEquals("John", result);
// Перевірка, що метод findById викликаний 1 раз
verify(repositoryMock, times(1)).findById(1);
}
}JavaПриклади, коли unit-тести життєво необхідні
- Критично важливі системи
У таких сферах, як медицина, фінанси, авіація чи автомобільна індустрія, помилки в програмному забезпеченні можуть мати катастрофічні наслідки. Unit-тести — це інтегральний компонент забезпечення якості у таких системах. - Робота у великих командах/проєктах
У великих командах, де багато розробників працюють паралельно над різними компонентами, unit-тести допомагають підтримувати стабільність системи, навіть якщо над кодом працює багато людей. - Підтримка довгострокових проєктів
Коли програмний продукт підтримується роками, баги, внесені в ранніх стадіях, можуть стати важковиявними. Unit-тести гарантують, що навіть старий код залишається перевіреним і стабільним.
Поширені міфи про unit-тести
- “Це забирає занадто багато часу”
Насправді якісні тести економлять час у майбутньому, оскільки швидше знаходять дефекти. - “Мій код і так працює ідеально”
Жодний код не є ідеальним. Людський фактор, складність алгоритмів і зовнішні залежності майже завжди призводять до помилок. - “Тести не потрібні для невеликих проєктів”
Навіть у маленьких проєктах помилки можуть спричиняти значні витрати. Тести можна почати використовувати поступово навіть в умовах невеликих ресурсів.
Найкращі практики написання Unit Tests
- Тести мають бути ізольованими. Не дозволяйте тестам залежати один від одного.
- Робіть короткі та зрозумілі назви для тестів. Наприклад:
testAdd_SuccessScenario. - Покривайте критичний функціонал. Фокусуйтеся на основних і критичних частинах коду.
- Використовуйте Mock для зовнішніх або важкодоступних залежностей.
- Запускайте тести регулярно. Інтегруйте тести в CI/CD.
- Організуйте тести відповідно до структури проєкту. Розміщуйте тести у відповідних модулях/пакетах.
І на сам кінець
Unit тестування є обов’язковою практикою для створення надійного програмного забезпечення. Інструменти, як JUnit і Mockito, спрощують процес тестування в Java-додатках. Дотримуйтесь найкращих практик, пишіть якісні тести, і ви зможете створювати програми, які легко підтримувати, масштабувати та використовувати.
Що станеться, якщо не писати Unit Tests?
Відмова від написання Unit тестів може призвести до таких проблем:
- Код стає менш стабільним, оскільки помилки не виявляються достатньо рано.
- Процес рефакторингу стає складним і ризикованим.
- Полагодження багів займає більше часу, ніж потрібно.
- Ви витрачаєте дорогоцінний час на повторне тестування вручну.
- Велика ймовірність виникнення проблем у продуктивному середовищі (production environment), що може призвести до втрати користувачів і коштів.
Незалежно від розміру вашого проєкту, інвестування часу у написання якісних Unit тестів — це гарантія успіху та довговічності вашого коду! У наступних статтях ми розберемо також як писати інтеграційні тести.





