• Час читання ~7 хв
  • 06.03.2023

Тестування коду програми - це абсолютно навичка, якій ви повинні навчитися. Це значно підвищує вашу власну впевненість і робить вашу програму набагато більш ремонтопридатною в довгостроковій перспективі. Знущання є важливою частиною тестування вашого коду. Однак також може бути трохи важко повністю зрозуміти концепцію. У цій статті я покажу вам всі поширені способи знущання над предметами в Laravel.

Що таке глузування?

Перш за все, що таке глузування? Знущання означає, що ви «підробляєте» клас. В основному це зводиться до того, що ви вибираєте певний клас і замінюєте його так званим макетом.

Отже, що таке макет? Макет - це «підроблена версія» об'єкта. Скажіть, що у вас є клас, який надсилає електронний лист користувачеві щоразу, коли розміщується замовлення. Це може виглядати трохи так:

<?php
namespace App\Actions\Order;

use App\Models\Order;

class SendOrderInformationAction
{
    public function execute(Order $order): void
    {
        $order->user->notify(/* ... */);
    }
}

І це буде використано так:

<?php
namespace App\Actions\Order;

use App\DataTransferObjects\OrderDTO;

class CreateOrderAction
{
    public function __construct(
        public SendOrderInformationAction $sendOrderInformationAction,
    ) {}
    public function execute(OrderDTO $order): void
    {
        // Do some stuff

        // Create order in database, etc.
        $order = /* ... */

        $this->sendOrderInformationAction->execute($order);
    }
}

Тепер ми хочемо перевірити, чи справді електронний лист надсилається щоразу, коли замовлення було завершено. Ми могли б додати сповіщення::fake() до CreateOrderTest і зробити деякі сповіщення::assertSent(...) матеріали пізніше.

Однак це залишає нам великий недолік: ми тестуємо поведінку SendOrderInformationAction, хоча насправді ми хочемо протестувати клас CreateOrderAction. Не зрозумійте мене неправильно: це справедливий підхід. Але якщо ви це зробите, ваш тестовий набір стане досить безладним, і ви майже напевно втратите огляд.

Рішення тут полягає в використанні макета. Використовуючи макет, ми можемо стверджувати, що SendOrderInformationAction називався, але не те, що зробив цей інший клас. Тепер ми також можемо створити SendOrderInformationActionTest, і цей тест буде стверджувати, чи правильно робить клас.

Отже, резюмуючи:

  1. Спочатку ми перевіряємо, чи використовувався SendOrderInformationAction.

  2. По-друге, в SendOrderInformationActionTest ми перевіряємо, чи правильно цей клас робить.

Тестуючи таким чином, ви зберігаєте свій testsuite clea nі набагато більше підтримуваного. І щоразу, коли щось ламається у вашому testsuite, ви відразу побачите, де проблема, замість того, щоб копати свій код.

How to mock a Laravel dependency

Давайте тепер перевіримо приклад того, як виглядає глузування. Глузування в Laravel виглядає так:

$this->mock(SendOrderInformationAction::class, function (MockInterface $mock) {
    $mock
        ->shouldReceive('execute')
        ->once()
        ->andReturn(true);
});
app(CreateOrderAction::class)->execute(/* ... */);

Тут є дві речі:

  1. Першим параметром для $this->mock() є назва класу, над яким ви хочете знущатися.

  2. Другий параметр - це замикання. Це замикання отримує змінну $mock. Змінна $mock може бути використана для вказівки того, які методи ми очікуємо, що te буде викликаний на макет.

В основному це означає дві речі:

  1. Ми говоримо Mockery побудувати «підроблений» об'єкт, який очікує, що функція буде виконана один раз і повернеться вірною.

  2. Водночас ми повідомляємо Laravel, що всякий раз, коли запитується SendOrderInformationAction::class, він повинен повернути підроблений об'єкт, згенерований знущанням.

Where are the Mockery assertions?

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

Defining Mockery expectations

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

`$mock->shouldReceive(...)`

Ви можете використовувати метод $mock->shouldReceive(), щоб сказати, що ми очікуємо виклику функції. У більшості випадків ви починаєте саме з цієї функції. Ви можете визначити кілька очікувань нижче один одного:Ви можете використовувати

$mock->shouldReceive('prepare');
$mock->shouldReceive('execute');
$mock->shouldReceive('cleanup');

наступні методи для ланцюжка після того, як слідReceive():

`->with(...)`

Ви можете використовувати метод ->with(...) для визначення аргументів, які слід надати функції. Отже, якщо функція отримує логічне та ціле число, ви можете визначити її так:

$mock->shouldReceive('execute')->with(false, 8)->once(0;

Ви також можете порівнювати об'єкти, але будьте попереджені, що вона вийде з ладу, якщо об'єкти не посилаються на один і той же екземпляр. Так що це не вдасться:

// In the test
$user = User::factory()->create();

$mock
   ->shouldReceive('execute')
   ->with($user)
   ->once();

// The function that we're mocking:
public function execute(bool $shouldBecomeSuperAdmin, User $user) { /** */ }

// Somewhere else:
$user = User::find($id);

app(ConvertUserToSuperAdminAction::class)->execute($user);

// FAIL

Чому? Оскільки об'єкт $user є новим об'єктом, який знову витягується десь в іншому місці.

На щастя, є спосіб виправити це: замість того, щоб безпосередньо вказувати $user, ви також можете вказати Mockery::on(...) як аргумент. Знущання::on(...) отримує зворотний дзвінок. Щоразу, коли Знущання захоче порівняти аргумент, він дасть аргумент зворотному дзвінку. Зворотний дзвінок повинен повертати істинний або помилковий, незалежно від того, чи дійсно це правильний аргумент. Це дуже гнучко і дозволяє нам це зробити:

// In the test
$user = User::factory()->create();

$this->mock(ConvertUserToAdminAction::class, function (MockInterface $mock) use ($user) {
    $mock
        ->shouldReceive('execute')
        ->with(true, Mockery::on(function (User $argument) use ($user) {
            return $argument->is($user);
        }));
});
app(ConvertUserToSuperAdminAction)->execute($user);

// PASS

`->once()`

Використовуйте модифікатор ->once(), щоб переконатися, що метод викликається лише один раз.

`->never()`

Використовуйте модифікатор ->never(), щоб переконатися, що метод ніколи не викликається.

`->andReturn(...)`

Використовуйте модифікатор ->andReturn(...), щоб вказати значення, яке слід повернути. Це може бути все, що завгодно.

`->andThrow(...)`

Ви можете використовувати метод ->andThrow(...), щоб визначити виняток, який слід кинути при виклику функції. Це може бути корисно для ситуацій, коли у вас є клас дій, який підключається до API. Ви також повинні протестувати ситуації, коли API недоступний або коли у вас обмежена ставка. Цей помічник може допомогти вам перевірити, як обробляються помилки.

Якщо ви хочете побачити всі методи, які ви можете використовувати (а їх багато), вам слід ознайомитися з цільовим показником Знущання над очікуваннями.

Default Laravel mocks

Laravel також надає багато спеціальних «макетів» з коробки. Ви можете не розглядати їх як глузування, тому що насправді вони є підробленими реалізаціями, визначеними Laravel. Однак, використовувати їх важливо (і легко) також.

Надані макети:

  1. Автобус::підробка() для тестування черг і завдань

  2. Подія::fake() для перевірки того, чи були (чи ні) відправлені

  3. правильні події

    Сповіщення::fake() для перевірки того, чи були надіслані

  4. (чи ні) правильні сповіщення

    Mail::fake() для тестування електронної пошти

  5. Http::fake() для підробки HTTP-запитів

  6. Зберігання::fake() для тестування сховища

  7. Черга::підробка() для тестування завдань. У більшості випадків слід використовувати автобус::fake().

Custom mocks

Якщо вам потрібен більш дрібнозернистий контроль над вашими класами макетів, ви також можете розглянути можливість використання макета ручної роботи. Це означає, що ви створюєте спеціальний клас для свого макета. Замість того, щоб вказувати очікування на $mock, тепер ви можете написати власну підроблену реалізацію. Зауважте, що це слід робити лише в рідкісних випадках, оскільки це збільшує складність у вашому testsuite.

Для того, щоб скористатися цим, вам потрібно буде створити новий клас. Цей клас розширює ваш базовий клас. Додайте нову статичну функцію під назвою setUp() (або як вам це потрібно). Ця функція повинна прив'язувати новий екземпляр користувацького підробленого класу до контейнера, щоразу, коли запитувався екземпляр батьківського класу.

Далі ви можете просто перевизначити всі методи (або лише ті, які ви хочете) і помістити туди свою власну логіку. Дивіться наступний приклад:

class ConvertUserToSuperAdminActionFake extends ConvertUserToSuperAdminAction
{
    public static function setUp(): void
    {
        app()->instance(ConvertUserToSuperAdminAction::class, new static());
    }
    public function execute(User $user) : void
    {
        // Now you can override this function with your custom logic.
    }
}
// In our test
ConvertUserToSuperAdminActionFake::setUp();

// Now everywhere the ConvertUserToSuperAdminActionFake::class is used
// instead of the normal ConvertUserToSuperAdminAction::class.

Always use the Laravel container

Є одна остання примітка, яку я хочу сказати вам, яка полягає в тому, щоб завжди використовувати контейнер Laravel, з ін'єкцією залежності або місцем обслуговування. У наведених вище прикладах ви побачили, що я використовував дві речі, щоб отримати правильний клас:

  1. Я використовував ін'єкцію залежності Laravel в конструкторі (перший приклад).

  2. Я використовував службове розташування через додаток (MyClass::class), щоб отримати екземпляр класу (другий приклад).

Використовуйте одну з цих методів, і Laravel надасть вам правильний екземпляр класу. Роблячи це так, легко замінити клас макетом.

Якщо ви пишете код, подібний до наступного, майже неможливо замінити MyClass::class на mock:

// Don't do this!
$object = new MyClass();

// But do this:
$object = app(MyClass::class);

Mocks v spies

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

З мого досвіду, ви будете рідше використовувати шпигуна, тому що у вас все ще є проблема перевірки поведінки класу B в тесті для класу А. Проте вони також мають своє застосування. Ось приклад:

$spy = $this->spy(ConvertUserToSuperAdminAction::class);

$spy
    ->shouldHaveReceived('execute')
    ->once();

Conclusion

Як ви бачили, знущатися над Laravel з знущаннями не так вже й складно. Це просто вимагає правильних трюків для навчання.

Особисто, коли я почав тестувати, я виявив, що висміюю концепцію, яку важко зрозуміти. Але через кілька місяців це поступово почало мати для мене сенс. Тепер я б не хотів тестувати програму без використання макетів!

Сподіваюся, це було корисно для вас! Я закликаю вас почати використовувати макети та протестувати свій додаток Laravel. Як завжди, якщо у вас є які-небудь питання або ідеї, будь ласка, залиште коментар або дайте мені знати!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Про автора CrazyBoy49z
WORK EXPERIENCE
Контакти
Ukraine, Lutsk
+380979856297