<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
ТЕСТУВАННЯ · JUNIOR

Яким має бути хороший unit-тест?

Хороший unit-тест перевіряє одну поведінку через публічний інтерфейс, складається з трьох частин (arrange, act, assert), має назву-речення про правило, не залежить від часу, бази й інших тестів і падає лише тоді, коли поведінка справді зламалась.

Напишіть тест на метод `total()` замовлення. Як ви його структуруєте?
Що таке arrange, act, assert?
Чи можна мати кілька перевірок в одному тесті?
Як перевірити приватний метод?
unit-тести arrange act assert Pest PHPUnit datasets

Unit-тест варто починати з доменного обʼєкта, який можна створити через new без бази, контейнера й фасадів. Тоді тест іде мілісекунди і падає лише від зміни логіки в цьому класі. Якщо клас так не створюється, це вже інформація: він сам викликає now(), app() чи модель Eloquent, і це питання дизайну, а не тесту.

Майже кожен хороший тест має три частини: arrange готує обʼєкт і дані, act виконує одну дію, заради якої написано тест, assert перевіряє результат. У BDD те саме звуть given, when, then. Порожні рядки між частинами роблять тест читабельним без коментарів. Якщо в act кілька дій із перевірками між ними, у тесті кілька поведінок, і їх варто розділити: коли перша перевірка впаде, решта навіть не виконається.

Одиниця тесту це поведінка, а не метод і не кількість expect(). Перевірити суму й кількість рядків після однієї дії нормально. Погано, коли в одному тесті три правила й назва «works». Назва має пояснити, що зламалось: «charges delivery only below the free delivery threshold» у звіті CI каже все, а «test order» змушує відкривати код. Список назв тестів класу добре читається як перелік його правил.

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

Решта властивостей хорошого тесту про довіру. Він детермінований: час і випадковість приходять ззовні, дані задані явно. Він незалежний: не спирається на те, що інший тест щось створив. Очікуване значення записане літералом, а не пораховане тією ж формулою, що й код. І він падає від справжньої помилки: якщо замінити >= на > у порозі доставки, тест на межі 2000 грн має почервоніти.

// tests/Unit/OrderTest.php: доменний обʼєкт без бази й фреймворку.

it('adds delivery to the sum of the lines', function () {
    // arrange
    $order = new Order(delivery: 50);
    $order->add('Чашка', price: 450, quantity: 2);
    $order->add('Кава', price: 300, quantity: 1);

    // act
    $total = $order->total();

    // assert: очікуване літералом, а не формулою з коду
    expect($total)->toBe(1250);
});

it('charges delivery only below the free delivery threshold', function (int $price, int $total) {
    $order = new Order(delivery: 50);
    $order->add('Кава', price: $price, quantity: 1);

    expect($order->total())->toBe($total);
})->with([
    'на гривню менше за поріг' => [1999, 2049],
    'рівно на порозі' => [2000, 2000],               // межу перевіряємо з обох боків
]);

it('rejects a zero quantity', function () {
    $order = new Order(delivery: 50);

    expect(fn () => $order->add('Чай', price: 100, quantity: 0))
        ->toThrow(InvalidArgumentException::class);
});
Структура arrange, act, assert (given, when, then) і одна дія в act; якщо дій кілька, це кілька тестів.
Один тест перевіряє одну поведінку, а не один метод і не один `expect()`: кілька перевірок результату однієї дії нормальні.
Назва як речення про правило (`it('charges delivery only below the free delivery threshold')`), щоб падіння в CI було зрозуміле без відкриття коду.
Тест бачить лише публічну поведінку: приватні методи перевіряються через публічні, а бажання їх тестувати означає, що клас робить забагато.
Детермінованість: час і випадковість приходять ззовні, тест не залежить від порядку запуску й сусідів.
Тестувати геттери й сетери: такий тест не впаде від справжньої помилки, але створює ілюзію покриття.
Читати приватну властивість через Reflection, щоб перевірити масив рядків замовлення. Після переходу на колекцію тест падає, хоча `total()` рахує так само.
Одна велика функція `it('works')` з трьома правилами: перша невдала перевірка ховає решту, а назва нічого не каже.
Порівнювати через `toEqual()` там, де потрібен `toBe()`: `toEqual(1250)` пропустить `1250.0` і рядок `'1250'`, бо порівнює як `==`.
Копіювати логіку з коду в тест: очікуване значення рахують тією ж формулою, тож помилка в формулі є і там, і там. Очікуване пишуть літералом.
ПОРАДА

Покажіть тест на межі правила: «доставка безкоштовна від 2000 грн» перевіряю на 1999 і на 2000, бо помилка в `>=` чи `>` ховається саме там. Це одразу видно, що ви думаєте про крайні випадки, а не пишете тест заради покриття.

оновлено 8 жовтня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

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