Тести, яким можна вірити
Які бувають тести, що чим перевіряти і як не потонути в моках. Pest, PHPStan і архітектурні тести на прикладах з PHP.
Піраміда Майка Кона (Mike Cohn): багато швидких unit-тестів і мало повільних. Класична порада для коду з багатою доменною логікою.
Трофей Кента Доддса (Kent C. Dodds): найбільше інтеграційних тестів, а в Laravel це feature-тести. Вони швидкі й ловлять те, що бачить користувач. В основі статичний аналіз.
Частки й час приблизні, це орієнтир, а не норма.
it('shows the vacancy page', function () { visit('/jobs/senior-laravel') ->assertSee('Відгукнутися') ->click('Відгукнутися') ->assertPathIs('/login'); });
it('lets a candidate withdraw an application', function () { $application = Application::factory()->create(); actingAs($application->candidate) ->post("/applications/{$application->id}/withdraw") ->assertRedirect(); expect($application->fresh()->isWithdrawn())->toBeTrue(); });
it('finds a saved application', function () { $repo = app(ApplicationRepository::class); $repo->save($application = ApplicationFactory::new()->make()); expect($repo->find($application->id))->not->toBeNull(); });
it('calculates the Nova Poshta rate', function () { $order = new Order(sum: 1200, weight: 3); expect((new NovaPoshtaRate)->quote($order))->toBe(96); });
vendor/bin/phpstan analyse --level=8 [OK] No errors
Рухайте повзунок
Один і той самий контролер на різних рівнях аналізу. Кожен рівень додає перевірки до попередніх, і в коді зʼявляються нові помилки.
Cannot access property $name on User|null.find() повертає ?User: користувача може не бути. Рівень 8 не дає звертатися до nullable-значення без перевірки на null.
Instantiated class WelcomMail not found.Класу WelcomMail ніде немає, схоже на одруківку. Невідомі класи PHPStan бачить уже на нульовому рівні.
Call to an undefined method Users::findOrFial().У Users є findOrFail, а findOrFial немає. Нульовий рівень перевіряє методи лише на $this, а на будь-якому виразі, як $this->users, з рівня 2.
Cannot call method notify() on mixed.Методу findOrFial немає, тож PHPStan не знає, що він повертає: це неявний mixed. Рівень 10 забороняє викликати методи й на ньому.
Method ProfileController::badge() has parameter $user with no type specified.Параметр без типу. Рівень 6 вимагає тип скрізь, де його можна вказати.
Method ProfileController::badge() should return string but returns int.rank() повертає int, а метод обіцяє string. Типи повернення PHPStan звіряє з рівня 3.
Parameter #1 $id of method Users::rank() expects int, string given.rank() чекає int, а сюди передали рядок. Типи аргументів перевіряє рівень 5.
Cannot access offset 'title' on mixed.json_decode() повертає mixed. На рівні 9 з mixed нічого не можна робити, доки тип не звужено, навіть узяти елемент масиву.
Method ProfileController::title() should return string but returns mixed.Метод обіцяє string, а віддає mixed. Рівень 9 більше не вважає mixed сумісним з будь-яким типом.
Тест і те, що він каже
Три тести для перевізників зі сторінки Стратегії. Перемкніть на «Вивід», а потім зламайте тариф Укрпошти й подивіться, що каже Pest.
- return 35 + $order->weight * 8; + return 38 + $order->weight * 8;
Базовий тариф Укрпошти виріс з 35 до 38 грн, і посилка вагою 3 кг тепер коштує 38 + 3 × 8 = 62 замість 59.
PHPUnit, на якому стоїть Pest, спершу називає фактичне значення: «62 is identical to 59» означає «отримали 62, очікували 59».
Рядок зі стрілкою ➜ показує, де саме впала перевірка: восьмий рядок файлу з тестом. Два інші тести лишились зеленими, бо Нову пошту й Checkout зміна не зачепила.
<?php it('calculates the Nova Poshta rate', function () { expect((new NovaPoshtaRate)->quote(order(weight: 3)))->toBe(96); }); it('calculates the Ukrposhta rate', function () { expect((new UkrposhtaRate)->quote(order(weight: 3)))->toBe(59); }); it('adds the rate to the order sum', function () { $checkout = new Checkout(new FakeRate(price: 50)); expect($checkout->total(order(sum: 1200)))->toBe(1250); });
Каталог тем
Основи
Basics · 5 темЩо таке тест, які бувають тести й чим підміняти залежності.
Unit, integration, feature, e2e: що кожен перевіряє й скільки коштує.
Тест без бази й фреймворку: обʼєкт, дія, перевірка.
Dummy, stub, spy, mock, fake без плутанини.
In-memory репозиторій замість бази й фейковий клієнт замість API.
FrozenClock, seed і тести, що не падають щопʼятниці.
Інструменти
Tools · 3 темиPest, вбудовані фейки Laravel і тести в Symfony.
expect(), datasets, архітектурні й браузерні тести.
Mail::fake, Queue::fake, Http::fake, Storage::fake.
KernelTestCase, WebTestCase і тестовий контейнер.
Якість коду
Code quality · 3 темиПеревірки, які ловлять помилки без жодного написаного тесту, і тести для самих тестів.
Домен не імпортує фреймворк, контролери не лізуть у базу.
Від нуля до десяти без болю: baseline і поступовий підйом.
Чи справді тести ловлять зміни в коді.
Архітектура й тести разом
Щойно зʼявляється нова ідея, ви одразу вчитеся її перевіряти. Цей самий маршрут є й на хабі архітектури.
-
1АРХІТЕКТУРАШари і де живе логікаінтерфейс, застосунок, домен, інфраструктура
-
2ТЕСТУВАННЯПерший unit-тест доменутест без бази й фреймворку
-
3АРХІТЕКТУРАValue Objectгроші, email, період як значення
-
4ТЕСТУВАННЯTest doublesdummy, stub, spy, mock, fake
-
5АРХІТЕКТУРАHexagonalпорти й адаптери
-
6ТЕСТУВАННЯФейки адаптерівin-memory замість бази й API
-
7ТЕСТУВАННЯАрхітектурні тестидомен не імпортує Laravel
-
8ТЕСТУВАННЯPHPStan і рівнівід нуля до десяти
-
9АРХІТЕКТУРАМодульний монолітмодулі з чіткими межами в одному репо
Що перевіряють перед кожним деплоєм цього сайту
deploy/deploy.sh
Перед кожним деплоєм запускає PHPStan і весь набір тестів Pest. Якщо хоч щось червоне, деплой зупиняється.
tests/Architecture/LayersTest.php
Не пускає Illuminate й Livewire у доменний і прикладний шари, домену забороняє ще й App, а в усьому src/ вимагає strict_types.
src/Shared/Clock/FrozenClock.php
Підміняє годинник у тестах: advance('+30 days') замість місяця очікування.
tests/Support/InMemoryJobRepository.php
In-memory JobRepository для unit-тестів домену, без жодної бази.
phpstan.neon
PHPStan рівня 8 з Larastan перевіряє app/ і src/.