Обʼєкт, дія, перевірка
Доменний обʼєкт тримає правила: як рахується сума замовлення, коли вакансія застаріває, чи можна скасувати оплачене. Якщо він не викликає базу, фасади й now(), його можна створити через new прямо в тесті. Такий тест не потребує міграцій, контейнера й мережі, тому йде за мілісекунди й падає лише тоді, коли змінилось саме правило.
Майже кожен unit-тест має три частини, і їх звуть . Arrange готує обʼєкт і його дані. Act робить рівно одну дію, заради якої написано тест. Assert перевіряє результат. Порожній рядок між частинами допомагає читачеві побачити, де закінчилась підготовка й що саме перевіряють.
Один тест перевіряє . Коли тест падає, його назва вже має пояснити, що зламалось: «adds delivery to the sum of the lines» каже більше за «test order». У Pest назва пишеться як речення після it(), тому читається як специфікація, а набір тестів одного класу стає списком його правил.
Тест перевіряє публічну поведінку обʼєкта, а не його нутрощі. Якщо для перевірки хочеться прочитати приватну властивість чи викликати приватний метод, обʼєкту бракує публічного методу, або ви перевіряєте реалізацію. Тест, привʼязаний до реалізації, падає від кожного рефакторингу, навіть коли поведінка лишилась та сама.
Один сценарій, чотири записи
Order рахує суму рядків і додає доставку 50 грн, а від 2000 грн доставка безкоштовна. Перша вкладка показує, з чого часто починають, решта три показують, як записати ту саму перевірку чисто: у Pest, у PHPUnit і з dataset.
Запустіть і подивіться
-
1
рядки 3–27
Доменний обʼєкт без бази й фреймворку. Його створюють через new, і тесту більше нічого не треба.
-
2
рядки 29–45
Те, що роблять it() і expect() у Pest, у півтора десятка рядків: виконати тіло, зловити невдалу перевірку й надрукувати ✓ або ✗.
-
3
рядки 47–55
Arrange, act, assert, розділені порожніми рядками. Змініть 1250 на 1200 і запустіть ще раз: побачите, як читається падіння.
-
4
рядки 57–62
Окремий тест на окреме правило. Поріг перевірено рівно на межі, де помилка в >= чи > ховається найчастіше.
-
5
рядки 64–74
Виняток теж поведінка. У Pest його перевіряє один рядок з toThrow(), як у вкладці «Усе в одному» вище.
- Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
SalaryTest у наших тестах
Вилка зарплати в PHP Ukraine це value object Salary з src/Hiring/Domain/Job: межі від і до, валюта, форматування й середина вилки. Тест лежить у tests/Unit і створює обʼєкт через new, без бази й Laravel. Кожен тест перевіряє одне правило, а кілька expect() в одному ланцюжку перебирають випадки цього правила.
Що заважає першому тесту
Тест на геттери й сетери
Перший тест перевіряє, що setName() зберігає імʼя, а getName() його повертає. Такий тест ніколи не впаде від справжньої помилки, зате створює враження, що код покрито.
Натомість: починайте з правила, яке можна зламати: розрахунок, межа, заборонений перехід статусу. Геттери перевіряться самі, коли ними скористаються такі тести.
Доменний обʼєкт сам питає час і базу
Метод isExpired() викликає now() або DB::table(). Тест на нього вже не unit: треба піднімати Laravel, а результат залежить від дня запуску.
Натомість: передавайте час параметром, isExpiredAt($now), або через порт Clock, а дані завантажуйте до виклику доменного методу. Тоді обʼєкт створюється через new у будь-якому тесті.
Перевірка приватного стану
Тест читає приватну властивість через Reflection, щоб перевірити масив рядків замовлення. Після рефакторингу на колекцію тест падає, хоча total() рахує так само.
Натомість: перевіряйте те, що видно ззовні: результат публічних методів і винятки. Якщо без приватного стану нічого не перевірити, обʼєкту бракує публічного методу.
Назва, яка нічого не каже
test1, it works, test order. Коли такий тест падає в CI, треба відкрити код, щоб зрозуміти, яке правило зламалось.
Натомість: називайте тест реченням про поведінку: «charges delivery only below the free delivery threshold». Список назв має читатися як перелік правил класу.
Чи готовий клас до unit-тесту
- Arrange
- act
- Поведінка
- Одне правило, яке видно ззовні обʼєкта: що повертає метод за певних даних, який виняток кидає, як змінюється стан. Одна поведінка може потребувати кількох перевірок.
Питання зі співбесід
Три частини тесту. Arrange готує обʼєкт і дані, act виконує одну дію, заради якої написано тест, assert перевіряє результат. Чітка межа між ними показує, що саме тест перевіряє, а якщо act розростається на кілька дій, тест, найімовірніше, перевіряє кілька поведінок.
toBe() порівнює строго, як ===: той самий тип і значення, а для обʼєктів той самий екземпляр. toEqual() порівнює як ==, тож два різні обʼєкти з однаковими властивостями рівні. Для чисел і рядків беріть toBe(), щоб 1250.0 не пройшло за 1250, для value objects зазвичай toEqual().
Так, якщо всі вони перевіряють одну поведінку: результат і стан після однієї дії або кілька випадків одного правила. Погано, коли в одному тесті кілька дій і кілька правил: назва вже не описує, що зламалось, а перша невдала перевірка ховає решту.
Приватні методи це реалізація, і тест на них падає від кожного рефакторингу, хоча поведінка лишилась та сама. Їх перевіряють опосередковано, через публічні методи, що ними користуються. Якщо приватна логіка така складна, що її хочеться тестувати окремо, це сигнал винести її в окремий клас з власним публічним API.