Архітектурні тести · Pest
Архітектурне тестування дає змогу описати очікування, які перевіряють, чи дотримується ваш застосунок набору архітектурних правил, і так допомагає тримати кодову базу чистою та придатною до розвитку. Очікування описуються через відносні простори імен, повні простори імен або назви функцій.
Ось приклад того, як можна описати архітектурне правило:
arch()
->expect('App')
->toUseStrictTypes()
->not->toUse(['die', 'dd', 'dump']);
arch()
->expect('App\Models')
->toBeClasses()
->toExtend('Illuminate\Database\Eloquent\Model')
->toOnlyBeUsedIn('App\Repositories')
->ignoring('App\Models\User');
arch()
->expect('App\Http')
->toOnlyBeUsedIn('App\Http');
arch()
->expect('App\*\Traits')
->toBeTraits();
arch()->preset()->php();
arch()->preset()->security()->ignoring('md5');
Тепер розберемо методи й модифікатори, доступні для архітектурного тестування. У цьому розділі ви дізнаєтесь про:
- Очікування: дають змогу описувати точні архітектурні правила.
- Пресети: дають змогу використовувати наперед визначені набори таких правил.
- Модифікатори: щоб виключити або проігнорувати певні типи файлів, класи, функції чи рядки коду.
Очікування
Точні очікування дають змогу описати конкретні архітектурні правила для вашого застосунку. Ось доступні очікування:
toBeAbstract()toBeClasses()toBeEnums()toBeIntBackedEnums()toBeInterfaces()toBeInvokable()toBeFinal()toBeReadonly()toBeStringBackedEnums()toBeTraits()toBeUsed()toBeUsedIn()toExtend()toExtendNothing()toImplement()toImplementNothing()toHaveMethodsDocumented()toHavePropertiesDocumented()toHaveAttribute()toHaveFileSystemPermissions()toHaveLineCountLessThan()toHaveMethod()toHaveMethods()toHavePrivateMethodsBesides()toHavePrivateMethods()toHaveProtectedMethodsBesides()toHaveProtectedMethods()toHavePublicMethodsBesides()toHavePublicMethods()toHavePrefix()toHaveSuffix()toHaveConstructor()toHaveDestructor()toOnlyImplement()toOnlyUse()toOnlyBeUsedIn()toUse()toUseStrictEquality()toUseTrait()toUseTraits()toUseNothing()toUseStrictTypes()
toBeAbstract()
Метод toBeAbstract() перевіряє, що всі класи в заданому просторі імен абстрактні.
arch('app')
->expect('App\Models')
->toBeAbstract();
toBeClasses()
Метод toBeClasses() перевіряє, що всі файли в заданому просторі імен є класами.
arch('app')
->expect('App\Models')
->toBeClasses();
toBeEnums()
Метод toBeEnums() перевіряє, що всі файли в заданому просторі імен є переліками (enum).
arch('app')
->expect('App\Enums')
->toBeEnums();
toBeIntBackedEnums()
Метод toBeIntBackedEnums() перевіряє, що всі переліки в заданому просторі імен мають цілочисельний backing-тип.
arch('app')
->expect('App\Enums')
->toBeIntBackedEnums();
toBeInterfaces()
Метод toBeInterfaces() перевіряє, що всі файли в заданому просторі імен є інтерфейсами.
arch('app')
->expect('App\Contracts')
->toBeInterfaces();
toBeInvokable()
Метод toBeInvokable() перевіряє, що всі файли в заданому просторі імен викликні (invokable).
arch('app')
->expect('App\Actions')
->toBeInvokable();
toBeTraits()
Метод toBeTraits() перевіряє, що всі файли в заданому просторі імен є трейтами.
arch('app')
->expect('App\Concerns')
->toBeTraits();
toBeFinal()
Метод toBeFinal() перевіряє, що всі класи в заданому просторі імен позначені як final.
arch('app')
->expect('App\ValueObjects')
->toBeFinal();
Зауважте, що це очікування зазвичай поєднують із модифікатором classes(), щоб перевірити, що всі класи в заданому просторі імен фінальні.
arch('app')
->expect('App')
->classes()
->toBeFinal();
toBeReadonly()
Метод toBeReadonly() перевіряє, що певні класи незмінні й не можуть бути модифіковані під час виконання.
arch('app')
->expect('App\ValueObjects')
->toBeReadonly();
Зауважте, що це очікування зазвичай поєднують із модифікатором classes(), щоб перевірити, що всі класи в заданому просторі імен readonly.
arch('app')
->expect('App')
->classes()
->toBeReadonly();
toBeStringBackedEnums()
Метод toBeStringBackedEnums() перевіряє, що всі переліки в заданому просторі імен мають рядковий backing-тип.
arch('app')
->expect('App\Enums')
->toBeStringBackedEnums();
toBeUsed()
Модифікатор not у поєднанні з методом toBeUsed() дає змогу переконатися, що певні класи або функції не використовуються у вашому застосунку.
arch('globals')
->expect(['dd', 'dump'])
->not->toBeUsed();
arch('facades')
->expect('Illuminate\Support\Facades')
->not->toBeUsed();
toBeUsedIn()
Поєднавши модифікатор not із методом toBeUsedIn(), можна заборонити використання конкретних класів і функцій у заданому просторі імен.
arch('globals')
->expect('request')
->not->toBeUsedIn('App\Domain');
arch('globals')
->expect('Illuminate\Http')
->not->toBeUsedIn('App\Domain');
toExtend()
Метод toExtend() перевіряє, що всі класи в заданому просторі імен успадковують конкретний клас.
arch('app')
->expect('App\Models')
->toExtend('Illuminate\Database\Eloquent\Model');
toExtendNothing()
Метод toExtendNothing() перевіряє, що всі класи в заданому просторі імен не успадковують жодного класу.
arch('app')
->expect('App\ValueObjects')
->toExtendNothing();
toImplement()
Метод toImplement() перевіряє, що всі класи в заданому просторі імен реалізують конкретний інтерфейс.
arch('app')
->expect('App\Jobs')
->toImplement('Illuminate\Contracts\Queue\ShouldQueue');
toImplementNothing()
Метод toImplementNothing() перевіряє, що всі класи в заданому просторі імен не реалізують жодного інтерфейсу.
arch('app')
->expect('App\ValueObjects')
->toImplementNothing();
toHaveMethodsDocumented()
Метод toHaveMethodsDocumented() перевіряє, що всі методи в заданому просторі імен задокументовані.
arch('app')
->expect('App')
->toHaveMethodsDocumented();
toHavePropertiesDocumented()
Метод toHavePropertiesDocumented() перевіряє, що всі властивості в заданому просторі імен задокументовані.
arch('app')
->expect('App')
->toHavePropertiesDocumented();
toHaveAttribute()
Метод toHaveAttribute() перевіряє, що певний клас має конкретний атрибут.
arch('app')
->expect('App\Console\Commands')
->toHaveAttribute('Symfony\Component\Console\Attribute\AsCommand');
toHaveFileSystemPermissions()
Метод toHaveFileSystemPermissions() перевіряє, що всі файли в заданому просторі імен мають конкретні права доступу у файловій системі.
arch('app')
->expect('App')
->not->toHaveFileSystemPermissions('0777');
toHaveLineCountLessThan()
Метод toHaveLineCountLessThan() перевіряє, що всі файли в заданому просторі імен мають менше рядків, ніж вказане значення.
arch('app')
->expect('App\Models')
->toHaveLineCountLessThan(100);
toHaveMethod()
Метод toHaveMethod() перевіряє, що певний клас має конкретний метод.
arch('app')
->expect('App\Http\Controllers\HomeController')
->toHaveMethod('index');
toHaveMethods()
Метод toHaveMethods() перевіряє, що певний клас має конкретні методи.
arch('app')
->expect('App\Http\Controllers\HomeController')
->toHaveMethods(['index', 'show']);
toHavePrivateMethodsBesides()
Метод toHavePrivateMethodsBesides() перевіряє, що певний клас не має жодних приватних методів, окрім вказаних.
arch('app')
->expect('App\Services\PaymentService')
->not->toHavePrivateMethodsBesides(['doPayment']);
toHavePrivateMethods()
Метод toHavePrivateMethods() перевіряє, що певний клас не має жодних приватних методів.
arch('app')
->expect('App\Services\PaymentService')
->not->toHavePrivateMethods();
toHaveProtectedMethodsBesides()
Метод toHaveProtectedMethodsBesides() перевіряє, що певний клас не має жодних захищених методів, окрім вказаних.
arch('app')
->expect('App\Services\PaymentService')
->not->toHaveProtectedMethodsBesides(['doPayment']);
toHaveProtectedMethods()
Метод toHaveProtectedMethods() перевіряє, що певний клас не має жодних захищених методів.
arch('app')
->expect('App\Services\PaymentService')
->not->toHaveProtectedMethods();
toHavePublicMethodsBesides()
Метод toHavePublicMethodsBesides() перевіряє, що певний клас не має жодних публічних методів, окрім вказаних.
arch('app')
->expect('App\Services\PaymentService')
->not->toHavePublicMethodsBesides(['charge', 'refund']);
toHavePublicMethods()
Метод toHavePublicMethods() перевіряє, що певний клас не має жодних публічних методів.
arch('app')
->expect('App\Services\PaymentService')
->not->toHavePublicMethods();
toHavePrefix()
Метод toHavePrefix() перевіряє, що всі файли в заданому просторі імен мають конкретний префікс.
arch('app')
->expect('App\Helpers')
->not->toHavePrefix('Helper');
toHaveSuffix()
Метод toHaveSuffix() перевіряє, що всі файли в заданому просторі імен мають конкретний суфікс.
arch('app')
->expect('App\Http\Controllers')
->toHaveSuffix('Controller');
toHaveConstructor()
Метод toHaveConstructor() перевіряє, що всі файли в заданому просторі імен мають метод __construct.
arch('app')
->expect('App\ValueObjects')
->toHaveConstructor();
toHaveDestructor()
Метод toHaveDestructor() перевіряє, що всі файли в заданому просторі імен мають метод __destruct.
arch('app')
->expect('App\ValueObjects')
->toHaveDestructor();
toOnlyImplement()
Метод toOnlyImplement() перевіряє, що певні класи реалізують лише вказані інтерфейси.
arch('app')
->expect('App\Responses')
->toOnlyImplement('Illuminate\Contracts\Support\Responsable');
toOnlyUse()
Метод toOnlyUse() гарантує, що певні класи використовують лише вказані функції або класи. Наприклад, так можна перевірити, що ваші моделі залишаються компактними й залежать винятково від простору імен Illuminate\Database, а не, скажімо, відправляють завдання в чергу чи події.
arch('models')
->expect('App\Models')
->toOnlyUse('Illuminate\Database');
toOnlyBeUsedIn()
Метод toOnlyBeUsedIn() дає змогу обмежити використання конкретного класу або набору класів лише певними частинами застосунку. Наприклад, цим методом можна підтвердити, що ваші моделі використовуються тільки репозиторіями, а не контролерами чи сервіс-провайдерами.
arch('models')
->expect('App\Models')
->toOnlyBeUsedIn('App\Repositories');
toUse()
Поєднавши модифікатор not із методом toUse(), ви вказуєте, що файли в заданому просторі імен не повинні використовувати конкретні функції або класи.
arch('globals')
->expect('App\Domain')
->not->toUse('request');
arch('globals')
->expect('App\Domain')
->not->toUse('Illuminate\Http');
toUseStrictEquality()
Метод toUseStrictEquality() перевіряє, що всі файли в заданому просторі імен використовують строге порівняння. Інакше кажучи, що замість оператора == застосовується ===.
arch('models')
->expect('App')
->toUseStrictEquality();
Або, якщо вам натомість потрібно перевірити, що всі файли в заданому просторі імен не використовують строге порівняння, скористайтеся модифікатором not.
arch('models')
->expect('App')
->not->toUseStrictEquality();
toUseTrait()
Метод toUseTrait() перевіряє, що всі файли в заданому просторі імен використовують конкретний трейт.
arch('models')
->expect('App\Models')
->toUseTrait('Illuminate\Database\Eloquent\SoftDeletes');
toUseTraits()
Метод toUseTraits() перевіряє, що всі файли в заданому просторі імен використовують конкретні трейти.
arch('models')
->expect('App\Models')
->toUseTraits(['Illuminate\Database\Eloquent\SoftDeletes', 'App\Concerns\CustomTrait']);
toUseNothing()
Якщо потрібно вказати, що певні простори імен або класи не повинні мати жодних залежностей, скористайтеся методом toUseNothing().
arch('value objects')
->expect('App\ValueObjects')
->toUseNothing();
toUseStrictTypes()
Метод toUseStrictTypes() перевіряє, що всі файли в заданому просторі імен використовують строгі типи.
arch('app')
->expect('App')
->toUseStrictTypes();
Пресети
Писати архітектурні очікування з нуля буває довго, особливо на початку нового проєкту, коли ви просто хочете переконатися, що базові архітектурні правила виконуються.
Пресети - це наперед визначені набори точних очікувань, якими можна перевіряти архітектуру застосунку.
php
Пресет php - це наперед визначений набір очікувань, який підходить будь-якому PHP-проєкту. Він не привʼязаний до жодного фреймворку чи бібліотеки.
Він забороняє використання die, var_dump і подібних функцій, а також перевіряє, що ви не використовуєте застарілі функції PHP.
arch()->preset()->php();
Усі очікування, що входять до пресету php, можна знайти в нашому початковому коді.
security
Пресет security - це наперед визначений набір очікувань, який підходить будь-якому PHP-проєкту. Він не привʼязаний до жодного фреймворку чи бібліотеки.
Він перевіряє, що ви не використовуєте код, який може призвести до вразливостей у безпеці, наприклад eval, md5 і подібні функції.
arch()->preset()->security();
Усі очікування, що входять до пресету security, можна знайти в нашому початковому коді.
laravel
Пресет laravel - це наперед визначений набір очікувань для проєктів на Laravel.
Він перевіряє, що структура вашого проєкту дотримується відомих домовленостей Laravel: наприклад, що контролери мають публічними методами лише index, show, create, store, edit, update, destroy і завжди мають суфікс Controller, і так далі.
arch()->preset()->laravel();
Усі очікування, що входять до пресету laravel, можна знайти в нашому початковому коді.
strict
Пресет strict - це наперед визначений набір очікувань, який підходить будь-якому PHP-проєкту. Він не привʼязаний до жодного фреймворку чи бібліотеки.
Він перевіряє, що в усіх файлах увімкнено строгі типи, що всі класи фінальні, і не тільки.
arch()->preset()->strict();
Усі очікування, що входять до пресету strict, можна знайти в нашому початковому коді.
relaxed
Пресет relaxed - це наперед визначений набір очікувань, який підходить будь-якому PHP-проєкту. Він не привʼязаний до жодного фреймворку чи бібліотеки.
Він протилежний пресету strict: перевіряє, що в файлах не використовуються строгі типи, що класи не фінальні, і так далі.
arch()->preset()->relaxed();
Усі очікування, що входять до пресету relaxed, можна знайти в нашому початковому коді.
custom
Зазвичай створювати custom-пресет не потрібно, бо точні очікування можна описати методом arch(). Але якщо вам хочеться мати власний пресет, визначте його методом custom.
Це буває корисно, коли у вас є набір очікувань, який ви часто застосовуєте в кількох проєктах, або якщо ви автор плагіна й хочете дати своїм користувачам готовий набір очікувань.
pest()->presets()->custom('ddd', function () {
return [
expect('Infrastructure')->toOnlyBeUsedIn('Application'),
expect('Domain')->toOnlyBeUsedIn('Application'),
];
});
Усередині методу custom вам доступні простори імен застосунку за PSR-4 у першому аргументі колбека вашого замикання.
pest()->presets()->custom('silex', function (array $userNamespaces) {
var_dump($userNamespaces); // array(1) { [0]=> string(3) "App" }
return [
expect($userNamespaces)->toBeArray(),
];
});
Далі власний пресет використовується так: до методу preset() додається виклик з назвою пресету.
arch()->preset()->silex();
Шаблони (wildcards)
Починаючи з Pest 3.8, у метод expect() можна передавати шаблони, щоб зіставляти код у кількох просторах імен. Наприклад, якщо потрібно перевірити, що весь код у будь-якій підтеці Traits містить трейти:
arch()
->expect('App\*\Traits') // Увесь код у будь-якому просторі імен App\*\Traits, наприклад App\Models\Traits тощо.
->toBeTraits();
arch()
->expect('App\*\*\Traits') // Увесь код у будь-якому просторі імен App\*\*\Traits, наприклад App\A\B\Traits, App\C\D\Traits тощо.
->toBeTraits();
Модифікатори
Іноді потрібно застосувати очікування, але виключити з нього певні типи файлів або проігнорувати певні класи, функції чи конкретні рядки коду. Для цього є такі методи:
ignoring()
Описуючи архітектурні правила, методом ignoring() можна виключити простори імен або класи, які інакше потрапили б під дію правила.
arch()
->preset()
->php()
->ignoring('die');
arch()
->expect('Illuminate\Support\Facades')
->not->toBeUsed()
->ignoring('App\Providers');
Часом певні компоненти не варто вважати «залежностями», бо вони є частиною стандартної бібліотеки PHP. Щоб налаштувати, що саме вважати «нативним» кодом, і виключити його під час тестування, Pest дає змогу вказати, що ігнорувати.
Наприклад, якщо ви не хочете вважати Laravel «залежністю», викличте метод arch() у функції beforeEach() і відкиньте весь код у просторі імен «Illuminate». Так ви зосередитеся лише на справжніх залежностях свого застосунку.
// tests/Pest.php
pest()->beforeEach(function () {
$this->arch()->ignore([
'Illuminate',
])->ignoreGlobalFunctions();
});
classes()
Модифікатор classes() обмежує очікування лише класами.
arch('app')
->expect('App')
->classes()
->toBeFinal();
enums()
Модифікатор enums() обмежує очікування лише переліками.
arch('app')
->expect('App\Models')
->enums()
->toOnlyBeUsedIn('App\Models');
interfaces()
Модифікатор interfaces() обмежує очікування лише інтерфейсами.
arch('app')
->expect('App')
->interfaces()
->toExtend('App\Contracts\Contract');
traits()
Модифікатор traits() обмежує очікування лише трейтами.
arch('app')
->expect('App')
->traits()
->toExtend('App\Traits\Trait');
extending()
Модифікатор extending() обмежує очікування лише класами або інтерфейсами, які успадковують заданий клас.
arch('app')
->expect('App')
->extending(Model::class)
->toUseTrait(HasFactory::class);
implementing()
Модифікатор implementing() обмежує очікування лише класами, які реалізують заданий інтерфейс.
arch('app')
->expect('App')
->implementing(ShouldQueue::class)
->toUseTrait(Dispatchable::class);
using()
Модифікатор using() обмежує очікування лише класами, які використовують заданий трейт.
arch('app')
->expect('App')
->using(HasFactory::class)
->toExtend(Model::class);
У цьому розділі ви дізналися, як виконувати архітектурне тестування й перевіряти, що архітектура вашого застосунку чи бібліотеки відповідає заданому набору вимог. Далі: чи думали ви колись, як тестувати продуктивність свого коду? Розглянемо стрес-тестування.
Перекладаємо з офіційної документації, розділ за розділом, і не ховаємо недоперекладене. Помітили неточність у терміні чи реченні: напишіть, виправимо.