• Время чтения ~2 мин
  • 06.03.2023

Тестирование кода приложения — это абсолютно навык, которому вы должны научиться. Это значительно повышает вашу собственную уверенность и делает ваше приложение гораздо более удобным в обслуживании в долгосрочной перспективе. Мошенничество является важной частью тестирования кода. Тем не менее, также может быть немного трудно полностью понять концепцию. В этой статье я покажу вам все распространенные способы издевательства над объектами в Laravel.

What is mocking?

Прежде всего, что такое издевательство? Издевательство означает, что вы «подделываете» класс. Это в основном сводится к тому, что вы выбираете конкретный класс и заменяете его так называемым Mock.

Итак, что же такое макет? Макет — это «поддельная версия» объекта. Предположим, что у вас есть класс, который отправляет электронное письмо пользователю всякий раз, когда размещается заказ. Это может выглядеть примерно так:

<?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);
    }
}

Теперь мы хотим проверить, действительно ли электронное письмо отправляется всякий раз, когда заказ был завершен. Мы могли бы добавить Notification::fake() в CreateOrderTest и сделать некоторые Уведомления::assertSent(...) позже.

Однако это оставляет нам большой недостаток: мы тестируем поведение SendOrderInformationAction, хотя на самом деле мы хотим протестировать класс CreateOrderAction. Не поймите меня неправильно: это правильный подход. Но если вы сделаете это, ваш набор тестов станет довольно грязным, и вы почти наверняка потеряете обзор.

Решение здесь заключается в использовании макета. Используя макет, мы можем утверждать, что был вызван SendOrderInformationAction, но не то, что сделал этот другой класс. Теперь мы также можем создать SendOrderInformationActionTest, и этот тест будет утверждать, правильно ли работает класс.

Итак, подведем итоги:

  1. Во-первых, мы проверяем, использовался ли SendOrderInformationAction.

  2. Во-вторых, в Тесте SendOrderInformationActionTest мы проверяем, правильно ли поступает этот класс.

Тестируя таким образом, вы сохраняете свой тестовый костюм clean и гораздо более поддерживаемым. И всякий раз, когда что-то ломается в вашем тестовом костюме, вы сразу увидите, в чем проблема, вместо того, чтобы копаться в своем коде.

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 может быть использована для указания того, какие методы, как мы ожидаем, будут вызываться в макете.

Это в основном означает две вещи:

  1. Мы говорим Mockery создать «поддельный» объект, который ожидает, что функция выполнит один раз, а затем вернет true.

  2. В то же время мы говорим Laravel, что всякий раз, когда запрашивается класс SendOrderInformationAction::class, он должен возвращать поддельный объект, сгенерированный Mockery.

Where are the Mockery assertions?

Итак, вам может быть интересно: где утверждения? Мы не определяем регулярные утверждения, к которым вы привыкли. Фактически, тесты автоматически завершатся ошибкой, если ожидаемый вызов функции не будет записан. Таким образом, в приведенном выше примере тест

Defining Mockery expectations

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

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

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

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

Вы можете использовать следующие методы для связывания после shouldReceive():

`->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(...) получает обратный звонок. Всякий раз, когда Mockery хочет сравнить аргумент, он даст аргумент обратному вызову. Обратный вызов должен возвращать значение true или false, независимо от того, действительно ли это правильный аргумент. Это очень гибко и позволяет нам сделать следующее:

// 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(...) для определения исключения, которое должно создаваться при вызове функции. Это может быть полезно в ситуациях, когда у вас есть класс Action, который подключается к API. Вы также должны протестировать ситуации, когда API недоступен или когда ваша оценка ограничена. Этот помощник может помочь вам в тестировании обработки ошибок.

Если вы хотите увидеть все методы, которые вы можете использовать (а их много), вы должны проверить Mockery документация об ожиданиях.

Default Laravel mocks

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

Предоставленные макеты:

  1. Bus::fake() для тестирования очередей и заданий

  2. Event::fake() для проверки того, были ли отправлены (или нет) правильные события

  3. Уведомление::fake() для проверки того, были ли отправлены (или нет) правильные уведомления

  4. Mail::fake() для тестирования электронной почты

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

  6. Хранилище::fake() для тестирования хранилища

  7. Очередь::fake() для тестирования заданий. В большинстве случаев следует использовать Bus::fake().

Custom mocks

Если вам нужен более детальный контроль над классами макетов, вы также можете рассмотреть возможность использования пользовательского макета ручной работы. Это означает, что вы создаете выделенный класс для своего макета. Вместо того, чтобы указывать ожидания по $mock, теперь вы можете написать свою собственную поддельную реализацию. Обратите внимание, что это следует делать только в редких случаях, потому что это увеличивает сложность в вашем тестовом костюме.

Чтобы использовать это, вам нужно будет создать новый класс. Этот класс расширяет базовый класс. Добавьте новую статическую функцию с именем 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 макетом:Возможно,

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

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

Mocks v spies

вы также слышали о слове spies, когда говорили о насмешках. Шпион похож на макет, за исключением того, что шпион не подделывает реализацию. Он только записывает, какие функции были вызваны, и позже вы можете утверждать, были ли вызваны правильные функции.

По моему опыту, вы будете использовать шпиона реже, потому что у вас все еще есть проблема тестирования поведения класса B в тесте для класса A. Тем не менее, они также имеют свое применение. Вот пример:

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

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

Conclusion

Как вы видели, издеваться в Laravel с Mockery не так уж и сложно. Это просто требует правильных трюков, чтобы учиться.

Лично, когда я начал тестировать, я обнаружил, что издевательство над трудной для понимания концепцией. Но через несколько месяцев это постепенно начало иметь для меня смысл. Теперь я бы не хотел тестировать приложение без использования макетов!

Надеюсь, это было полезно для вас! Я бы посоветовал вам начать использовать макеты и протестировать свое приложение 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