TDD

Test Driven Development w Laravelu

Podczas tworzenia aplikacji lub nowej funkcjonalności, testy możemy pisać równolegle w trakcie jej tworzenia lub dopiero na samym końcu. W przypadku TDD sprawa ma się dokładnie na odwrót. Najpierw piszemy test do nieistniejącej funkcjonalności, a następnie piszemy kod który sprawi, że nasz test zakończy się pozytywnym wynikiem.

Oto jak w skrócie wygląda cykl TDD:

  1. Piszemy test do nieistniejącej funkcjonalność.
  2. Uruchamiamy test, który oczywiście kończy się błędem. Na podstawie wygenerowanego błędu tworzymy minimalny kod pozwalający na przejście testu.
  3. Po pozytywnym przejściu testu, rozwijamy nasz kod i ponownie uruchamiamy testy.

Cykl ten możemy również nazwać red, green, refactor. Pierwsze uruchomienie testu generuje błędy (red), po napisaniu kodu odpowiedzialnego za nową funkcjonalność ponowne uruchomienie testu kończy się wynikiem pozytywnym (green). Następnie rozwijamy nasz kod (refactor).

Na potrzeby tego artykułu załóżmy, że będziemy rozwijać aplikację o możliwość tworzenia edycji oraz usuwania postów.

Przygotowanie

W przypadku Laravela wiele rzeczy zostało już wstępnie przygotowanych i stworzenie nowego testu jest niezwykle łatwe. Aby rozpocząć, musimy zacząć od konfiguracji bazy danych. W pliku phpunit.xml, który znajduje się w głównym katalogu projektu między tagami , musimy dodać (lub w przypadku Laravela 8 odkomentować) taki kod:

<server name="DB_CONNECTION" value="sqlite"/>

<server name="DB_DATABASE" value=":memory:"/> 

Dzięki temu, w trakcie uruchamiania testu, nie będziemy korzystali z bazy danych, która na co dzień służy nam podczas tworzenia aplikacji. Następnie musimy utworzyć odpowiedni plik, w którym będziemy pisali testy. Jak zawsze w przypadku Laravela z pomocą przychodzi artisan: 

php artisan make:test PostTest

Tworzenie nowego zasobu

W nowo utworzonym pliku tests/Feature/PostTest.php stwórzmy nowy test, który będzie odpowiedzialny za tworzenie nowych zasobów.

  /** @test */

    public function a_post_can_be_created()
    {
        $response = $this->post('/posts', [
            'title' => 'This is a test post.',
            'body' => 'Some lorem ipsum text.'
        ]);

        $response->assertOk();
        $this->assertCount(1, Post::all());
    }

Zwróć uwagę na kilka istotnych kwestii:

  1. Komentarz zawierający @test. Jest to niezbędne do uruchomienia naszego testu.
  2. Nazwa metody lepiej, żeby była długa, ale dobrze opisywała czynności wykonywane w teście, niż krótka i nic nie znacząca.

Omówmy jeszcze, co zawiera nasz test. Po wysłaniu żądania typu POST pod adres / posts z danymi title oraz body, spodziewamy się odpowiedzi serwera o statusie 200 oraz że po pobraniu wszystkich postów z bazy danych będą one równe liczbie jeden.

Niestety, kiedy ponownie uruchomimy test, liczba postów w bazie danych będzie już równa 2 (post z poprzedniego testu oraz z obecnego). Potrzebujemy zatem wyczyścić bazę danych za każdym razem, gdy zakończmy test. Zwróć uwagę, że na górze pliku mamy już zaimportowany odpowiedni trait:

use Illuminate\Foundation\Testing\RefreshDatabase;

Potrzebujemy jedynie użyć go w naszej klasie:

use RefreshDatabase;

Spróbujmy zatem uruchomić nasz pierwszy test, robimy to za pomocą komendy:

./vendor/bin/phpunit --filter a_post_can_be_created

Oczywiście nasz test zakończy się niepowodzeniem, ponieważ nie mamy przygotowanego routingu, kontrolera ani modelu. Przygotujmy więc wszystkie niezbędne rzeczy i spróbujmy uruchomić nasz test ponownie, tworząc kontroler, model oraz routing.

Obsługa błędów

Czasami podczas uruchamiania testów zostanie nam zwrócony błąd, którego treść nie będzie zbyt pomocna i ciężko będzie na pierwszy rzut oka zrozumieć, co jest przyczyną:

1) Tests\Feature\PostTest::a_post_can_be_created
Response status code [500] does not match expected 200 status code.

Aby otrzymać bardziej szczegółowe informacje na temat błędu, musimy wyłączyć obsługę błędów przez Laravela. Dodajmy zatem w ciele naszej metody taki kod:

$this->withoutExceptionHandling();

Teraz po ponownym uruchomieniu testu otrzymamy informację, która będzie dla nas dużo bardziej przydatna:

1) Tests\Feature\PostTest::a_post_can_be_created
Illuminate\Database\Eloquent\MassAssignmentException: Add [title] to fillable property to allow mass assignment on [App\Models\Post]

Zmiana istniejącego zasobu

Stwórzmy teraz test sprawdzający możliwość zmiany już utworzonego postu. Dodajmy zatem nową metodę:

    /** @test */

    public function a_post_can_be_updated()
    {

    }

Przypominam, że podczas uruchamiania każdego testu, startujemy z czystą bazą, która nie zawiera żadnych danych. Tak więc nasz test musimy zacząć od stworzenia nowego zasobu, a następnie próby jego zmiany.

/** @test */

public function a_post_can_be_updated()
{
    $this->post('/posts', [
        'title' => 'Post title',
        'body' => 'Post body',
    ]);

    $this->assertCount(1, Post::all());
    $post = Post::first();

    $this->patch('/posts/' . $post->id, [
        'title' => 'New title',
        'body' => 'New body',
    ]);

}

Musimy teraz sprawdzić, czy tytuł oraz treść faktycznie uległa zmianie. W tym celu wystarczy, że pobierzemy pierwszy (i jedyny) post i porównamy jego zawartość:

$this->assertEquals('New title', Post::first()->title);
$this->assertEquals('New body', Post::first()->body);

Czas uruchomić nasz test i sprawdzić, jaki będzie jego wynik:

./vendor/bin/phpunit --filter a_post_can_be_updated

Ponownie, odpowiedź, która zostaje zwrócona, nie jest zbyt pomocna:

1) Tests\Feature\PostTest::a_post_can_be_updated
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'New title'
+'Post title’

Jednak teraz już wiemy, co robić w takim przypadku. Wyłączmy zatem obsługę błędów i ponownie spróbujmy przeprowadzić test. Tym razem powinniśmy otrzymać informację o braku odpowiedniego routingu. Teraz, zgodnie z metodologią TDD, powinniśmy stworzyć routing oraz metodę w kontrolerze odpowiedzialną za zapisanie zmian w poście. Jeśli wszystko wykonaliśmy prawidłowo, nasz test powinien zakończyć się powodzeniem.

Usunięcie zasobu

Test rozpoczniemy bardzo podobnie jak w przypadku modyfikacji zasobu. Z racji tego, że w naszej bazie nie znajdują się żadne rekordy, nasz test musimy rozpocząć od dodania nowego postu, aby następnie móc przetestować jego usunięcie. Tak więc zacznijmy od stworzenia nowej metody.

 /** @test */

public function a_post_can_be_deleted()
{
    $this->withoutExceptionHandling();
    $this->post('/posts', [
        'title' => 'Post title',
        'body' => 'Post body',
    ]);

    $this->assertCount(1, Post::all());
}

Następnie nie pozostaje nam nic innego, jak wysłać odpowiedni request i ponownie sprawdzić, czy tabela posts tym razem nie zawiera już żadnych rekordów.

$post = Post::first();
$this->delete('/posts/' . $post->id);
$this->assertCount(0, Post::all());

Oczywiście również i tym razem nasz test zakończy się niepowodzeniem. Na tym przecież polega TDD. Teraz dopiero jest czas na stworzenie odpowiedniego routingu oraz metody w kontrolerze, która obsłuży ten request.

Grupowanie testów

Przy większej ilości testów warto je pogrupować, aby w przyszłości nie musieć uruchamiać wszystkich na raz lub ręcznie każdego z osobna. Do tego celu nad każdą metodą w komentarzu oprócz @test należy dodać @gorup <nazwa grupy>. Na potrzeby tego artykułu dzisiaj tworzone testy nazwijmy post_test_group. W celu uruchomienia testów z tej właśnie grupy należy wprowadzić w terminalu:

./vendor/bin/phpunit --group post_test_group

Podsumowanie

Dzięki TDD błędy w aplikacji jesteśmy w stanie wykryć znacznie szybciej niż przy normalnym trybie pracy, kiedy to testy piszemy na sam koniec. Zaoszczędzimy również mnóstwo czasu na ewentualnie poprawie kodu. W cały proces będzie więc zaangażowanych mniej osób.Należy jednak pamiętać, że TDD nie sprawdzi się w przypadku małych aplikacji, a od zespołu programistów wymaga dodatkowych umiejętności, którymi na co dzień zajmuje się grupa QA.

W Droptica mamy wieloletnie doświadczenie w dostarczaniu usług PHP developmentu. Skontaktuj się z nami, jeśli chcesz dowiedzieć się więcej na temat Test Driven Development. Jeśli zaś interesuje Cię Laravel, zachęcam do zapoznania się z moim artykułem, w którym omawiam kwestię intuicyjności oraz szybkości w pisaniu kodu z użyciem Laravela.

3. Najlepsze praktyki zespołów programistycznych