• Czas czytania ~7 min
  • 06.03.2023

Testowanie kodu aplikacji jest absolutnie umiejętnością, której powinieneś się nauczyć. To znacznie poprawia Twoją pewność siebie i sprawia, że Twoja aplikacja jest znacznie łatwiejsza w utrzymaniu na dłuższą metę. Kpiny są ważną częścią testowania kodu. Jednak może być również nieco trudno w pełni zrozumieć tę koncepcję. W tym artykule pokażę ci wszystkie typowe sposoby kpiny z przedmiotów w Laravel.

What is mocking?

Po pierwsze, co to jest kpina? Kpina oznacza, że "udajesz" klasę. Zasadniczo sprowadza się to do tego, że wybierasz konkretną klasę i zastępujesz ją tak zwaną makietą.

Czym więc jest makieta? Makieta to "fałszywa wersja" obiektu. Załóżmy, że masz zajęcia, które wysyłają wiadomość e-mail do użytkownika za każdym razem, gdy składane jest zamówienie. Mogłoby to wyglądać trochę tak:

<?php
namespace App\Actions\Order;

use App\Models\Order;

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

I byłoby używane tak:

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

Teraz chcemy sprawdzić, czy wiadomość e-mail jest rzeczywiście wysyłana za każdym razem, gdy zamówienie zostało zakończone. Moglibyśmy dodać Notification::fake() do CreateOrderTest i zrobić trochę Notification::assertSent(...) rzeczy później.

Pozostawia nam to jednak duży minus: testujemy zachowanie SendOrderInformationAction, chociaż w rzeczywistości chcemy przetestować klasę CreateOrderAction. Nie zrozumcie mnie źle: jest to słuszne podejście. Ale jeśli to zrobisz, twój zestaw testów stanie się dość niechlujny i prawie na pewno stracisz przegląd.

Rozwiązaniem jest tutaj użycie makiety. Używając makiety, możemy stwierdzić, że SendOrderInformationAction został nazwany, ale nie to, co zrobiła ta inna klasa. Możemy teraz również utworzyć SendOrderInformationActionTest, a ten test potwierdzi, czy klasa robi to, co trzeba.

Podsumowując:

  1. Najpierw sprawdzamy, czy użyto SendOrderInformationAction.

  2. Po drugie, w SendOrderInformationActionTest testujemy, czy ta klasa robi to, co trzeba.

Testując w ten sposób, zachowujesz swój zestaw testowy clean i znacznie łatwiejszy w utrzymaniu. A gdy coś się zepsuje w twoim zestawie testów, natychmiast zobaczysz, gdzie jest problem, zamiast przekopywać się przez kod.

How to mock a Laravel dependency

Sprawdźmy teraz przykład, jak wygląda kpina. Kpina w Laravel wygląda

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

tak:Są tu dwie rzeczy:

  1. Pierwszy parametr $this->mock() to nazwa klasy, którą chcesz wypróbować.

  2. Drugi parametr to zamknięcie. To zamknięcie otrzymuje zmienną $mock. Zmienna $mock może być użyta do określenia, jakie metody mają być wywoływane na makiecie.

Zasadniczo oznacza to dwie rzeczy:

  1. Mówimy Mockery, aby skonstruował "fałszywy" obiekt, który oczekuje, że funkcja zostanie wykonana raz i zwróci prawdę.

  2. Jednocześnie mówimy Laravelowi, że za każdym razem, gdy żądany jest SendOrderInformationAction::class, powinien zwrócić fałszywy obiekt wygenerowany przez Mockery.

Where are the Mockery assertions?

Być może zastanawiasz się więc: gdzie są twierdzenia? Nie definiujemy regularnych twierdzeń, do których możesz być przyzwyczajony. W rzeczywistości testy automatycznie zakończą się niepowodzeniem, jeśli oczekiwane wywołanie funkcji nie zostanie zarejestrowane. Tak więc w powyższym przykładzie test

Defining Mockery expectations

przejdzie.Ważną częścią jest tutaj wiedzieć, jak zdefiniować oczekiwania dotyczące zmiennej $mock. Definiowanie oczekiwań jest bardzo proste i najczęściej odbywa się płynnie. Oto kilka ważnych funkcji, które powinieneś

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

znać:Możesz użyć metody $mock->shouldReceive(), aby powiedzieć, że oczekujemy wywołania funkcji. W większości przypadków zaczynasz od tej funkcji. Można zdefiniować wiele oczekiwań poniżej siebie:Można użyć

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

następujących metod do tworzenia łańcucha po shouldReceive():

`->with(...)`

Można użyć metody ->with(...) do zdefiniowania argumentów, które powinny być podane funkcji. Jeśli więc funkcja otrzymuje wartość logiczną i liczbę całkowitą, można ją zdefiniować w następujący sposób:

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

Można również porównywać obiekty, ale należy pamiętać, że nie powiedzie się, jeśli obiekty nie będą odwoływać się do tego samego wystąpienia. Więc to się nie powiedzie:

// 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

Dlaczego? Ponieważ obiekt $user jest nowym obiektem, który jest ponownie pobierany w innym miejscu.

Na szczęście istnieje sposób, aby to naprawić: zamiast bezpośrednio określać $user, możesz również podać Mockery::on(...) jako argument. Kpina::on(...) odbiera oddzwonienie. Ilekroć Mockery chce porównać argument, da argument do wywołania zwrotnego. Wywołanie zwrotne powinno zwrócić wartość true lub false, niezależnie od tego, czy jest to rzeczywiście właściwy argument. Jest to bardzo elastyczne i pozwala nam to zrobić:

// 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()`

Użyj modyfikatora ->once(), aby upewnić się, że metoda jest wywoływana tylko raz.

`->never()`

Użyj modyfikatora ->never(), aby upewnić się, że metoda nigdy nie zostanie wywołana.

`->andReturn(...)`

Użyj modyfikatora ->andReturn..., aby określić wartość, która ma zostać zwrócona. To może być wszystko, co chcesz.

`->andThrow(...)`

Można użyć metody ->andThrow(...) do zdefiniowania wyjątku, który powinien zostać zgłoszony podczas wywoływania funkcji. Może to być przydatne w sytuacjach, gdy masz klasę Action, która łączy się z interfejsem API. Należy również przetestować sytuacje, w których interfejs API jest niedostępny lub gdy masz ograniczoną stawkę. Ten pomocnik może pomóc w testowaniu sposobu obsługi błędów.

Jeśli chcesz zobaczyć wszystkie metody, których możesz użyć (a jest ich wiele), powinieneś sprawdzić dokumentację < href="https://docs.mockery.io/en/latest/reference/expectations.html" target="_blank" rel="noopener">Mockery na temat oczekiwań.

Default Laravel mocks

Laravel zapewnia również wiele niestandardowych "makietów" po wyjęciu z pudełka. Możesz nie postrzegać ich jako makiet, ponieważ w rzeczywistości są to fałszywe implementacje zdefiniowane przez Laravela. Niemniej jednak korzystanie z nich jest ważne (i łatwe).

Dostarczone makiety to:

  1. Bus::fake() do testowania kolejek i zadań

  2. Event::fake() do sprawdzania, czy (lub nie) zostały wywołane

  3. właściwe zdarzenia

    Powiadomienie::fake() do testowania, czy (lub nie) zostały wysłane

  4. właściwe powiadomienia

    Mail::fake() do testowania poczty e-mail

  5. Http::fake() do fałszowania żądań HTTP

  6. Storage::fake() do testowania magazynu

  7. Queue::fake() do testowania zadań. W większości przypadków należy używać Bus::fake().

Custom mocks

Jeśli potrzebujesz bardziej szczegółowej kontroli nad klasami makiet, możesz również rozważyć użycie niestandardowej, ręcznie wykonanej makiety. Oznacza to, że tworzysz dedykowaną klasę dla swojej makiety. Zamiast określać oczekiwania dotyczące $mock, możesz teraz napisać własną fałszywą implementację. Należy pamiętać, że należy to robić tylko w rzadkich przypadkach, ponieważ zwiększa to złożoność zestawu testów.

Aby z tego skorzystać, musisz utworzyć nową klasę. Ta klasa rozszerza klasę bazową. Dodaj nową funkcję statyczną o nazwie setUp() (lub w żądany sposób). Ta funkcja powinna powiązać nowe wystąpienie niestandardowej klasy fałszywej z kontenerem za każdym razem, gdy zażądano wystąpienia klasy nadrzędnej.

Następnie możesz po prostu zastąpić wszystkie metody (lub tylko te, które chcesz) i umieścić tam swoją niestandardową logikę. Zobacz następujący przykład:

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

Jest jeszcze jedna uwaga, którą chcę wam powiedzieć, a mianowicie, aby zawsze używać kontenera Laravel, z wstrzyknięciem zależności lub lokalizacją usługi. W powyższych przykładach zobaczyłeś, że użyłem dwóch rzeczy, aby uzyskać odpowiednią klasę:

  1. W konstruktorze użyłem iniekcji zależności Laravela (pierwszy przykład).

  2. Użyłem lokalizacji usługi za pośrednictwem app(MyClass::class), aby uzyskać instancję klasy (drugi przykład).

Użyj jednej z tych technik, a Laravel dostarczy ci poprawną instancję klasy. Robiąc to w ten sposób, łatwo jest zastąpić klasę makietą.

Jeśli piszesz kod podobny do poniższego, prawie niemożliwe jest zastąpienie MyClass::class mock:

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

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

Mocks v spies

You might also heard of the word spies, gdy mówimy o mockingu. Szpieg jest podobny do makiety, z tym wyjątkiem, że szpieg nie fałszuje implementacji. Rejestruje tylko, które funkcje zostały wywołane, a później można stwierdzić, czy zostały wywołane właściwe funkcje.

Z mojego doświadczenia wynika, że będziesz rzadziej korzystał ze szpiega, ponieważ nadal masz problem z testowaniem zachowania klasy B w teście na klasę A. Niemniej jednak mają one również swoje zastosowania. Oto przykład:

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

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

Conclusion

Jak widzieliście, kpiny w Laravel z kpiną nie są takie trudne. Wymaga tylko odpowiednich sztuczek, aby się nauczyć.

Osobiście, kiedy zacząłem testować, znalazłem kpinę z trudnej do zrozumienia koncepcji. Ale po kilku miesiącach stopniowo zaczęło to mieć dla mnie sens. Teraz nie chciałbym testować aplikacji bez użycia makietów!

Mam nadzieję, że to było dla Ciebie przydatne! Zachęcam do rozpoczęcia korzystania z makiet i przetestowania aplikacji Laravel. Jak zawsze, jeśli masz jakieś pytania lub pomysły, zostaw komentarz lub daj mi znać!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

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...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297