Wprowadzenie do testowania w Laravelu - część 1

Paweł Mysior
5 listopada 2019

W tej serii wpisów zbudujemy wspólnie nieskomplikowaną aplikację bloga od podstaw pisząc do niej testy automatyczne przy użyciu techniki TDD, czyli Test Driven Development. Technika TDD polega na tym, że najpierw piszemy dla danej funkcjonalności test automatyczny, a dopiero potem piszemy kod który tę funkcjonalność implementuje.

Stworzymy wspólnie aplikację bloga, w której napiszemy testy i implementację dla następujących funkcjonalności:

  • na liście wpisów wyświetlać się będą tylko wpisy oznaczone jako opublikowane
  • zalogowani użytkownicy mogą dodawać wpisy
  • tytuł wpisu musi być unikatowy, zawartość wpisu może być pusta, ale jeśli ma wartość to musi mieć minimum 3 znaki
  • zalogowani użytkownicy mogą edytować wpisy
  • zalogowani użytkownicy mogą usuwać wpisy

Na koniec przerobimy ostatnie ostatnie dwa podpunkty w ten sposób, żeby użytkownicy mogli edytować tylko swoje wpisy, natomiast usuwać wpisy będzie mógł tylko administrator.

W tej części zaczniemy od zainstalowania nowej aplikacji postawionej na Laravelu. Następnie przejdziemy przez krótkie wprowadzenie do tego czym jest i jak używa się narzędzia do testowania PHPUnit. Następnie zastanowimy się jaki powinien być pierwszy test w naszej aplikacji. Napiszemy test sprawdzający wybraną funkcjonalność, po czym napiszemy kod który ją zaimplementuje.

Kod aplikacji znajduje się na GitHubie.

Wymagania

Ten tutorial zakłada podstawową znajomość terminala, composera oraz samego Laravela w takich zakresach jak instalacja frameworka, tworzenie migracji tabeli, obsługa Eloquenta, definiowanie ścieżek, użycie kontrolerów, obsługa i walidacja formularzy oraz użycie silnika Blade. Innymi słowy, jeśli masz już za sobą chociaż jedną małą aplikację postawioną na Laravelu to dasz radę. Nowe i bardziej skomplikowane zagadnienia będę po kolei tłumaczył.

Instalacja

Zacznijmy od zainstalowania świeżej aplikacji za pomocą instalatora Laravela. Przejdźmy w terminalu do wybranego katalogu i wpiszmy:

laravel new posts

Następnie przejdźmy do nowo utworzonego katalogu i zainicjalizujmy repozytorium git:

cd posts
git init

Dobrą praktyką jest od razu dodanie całego kodu jako początkowego commita. Ten pierwszy commit to świeża, można powiedzieć pusta instalacja Laravela, w której jeszcze nic nie zmieniliśmy. Każdy kolejny commit będzie już zawierał zmiany związane stricte z naszą aplikacją. Dzięki temu w historii repozytorium będziemy wyraźnie widzieli, w których plikach dokonaliśmy zmian względem tej "pustej" instalacji Laravela.

git add .
git commit -m "Świeża instalacja Laravel 6.4.1"

Korzyści z pisania testów automatycznych

Zacznijmy od wyjaśnienia jednej kwestii: nawet jeśli w życiu nie słyszałeś o PHPUnit, to nie zmienia faktu, że testujesz swój kod. Codziennie programując wykonujesz dziesiątki albo i setki testów. Piszesz kawałek kodu, przełączasz się na przeglądarkę, odświeżasz, uzupełniasz pola formularza, a potem sprawdzasz w bazie danych czy wszystko się zgadza. Brzmi znajomo? To właśnie testowanie.

To co możesz zrobić za pomocą PHPUnit to zautomatyzować ten proces testowania. Dlatego właśnie mówi się o testach automatycznych. Jakie są korzyści pisania testów automatycznych? Po pierwsze, skraca to czas programowania. Owszem, najpierw trochę go wydłuża, bo pisać testy trzeba się nauczyć. Następnie trzeba je rzeczywiście pisać. Ale gdy mamy już napisany automatyczny test, sam proces przetestowania kodu implementującego daną funkcjonalność zajmie kilkaset milisekund. Nie musimy przełączać się na przeglądarkę, odpalać phpMyAdmin itd.

Po drugie, co ważniejsze, mając testy automatyczne możemy z dużą pewnością siebie wprowadzać zmiany w kodzie. Gdy dodajemy do istniejącej aplikacji nową funkcjonalność to zawsze jest ryzyko, że zepsujemy coś co już dobrze działa. Jeśli testujemy ręcznie możemy o czymś zapomnieć. Jeśli mamy testy automatyczne to po wprowadzeniu zmian po prostu uruchamiamy testy dla wcześniej napisanych funkcjonalności i jeśli testy przeszły to mamy pewność, że nic nie zepsuliśmy.

Po trzecie, testy stają się niejako dokumentacją naszego kodu. Często gdy wracamy do własnego kodu napisanego pół roku temu nie pamiętamy co robi jakiś kawałek kodu. Może jest tam jakiś if, który obsługuje jakiś dziwny warunek brzegowy (ang. edge-case). Jeśli poświęcimy czas na napisanie testu dla tego warunku brzegowego, będziemy mieli miejsce do którego będziemy mogli zajrzeć żeby przypomnieć sobie po co ten if tam jest. Argument, mówiący o tym że testy są dokumentacją kodu jest tym bardziej istotny gdy pracujemy w zespole.

Mógłbym dalej przytaczać korzyści, ale mam nadzieję, że już Was przekonałem.

Krótkie wprowadzenie do testowania za pomocą PHPUnit

W porządku, chcemy zacząć pisać testy automatyczne do naszego kodu. Jak to robimy? Standardem w świecie PHP jest narzędzie PHPUnit. Jest to framework, który pozwala nam pisać testy automatyczne oraz program (narzędzie konsolowe) do uruchamiania tych testów. PHPUnit to nic innego jak paczka PHP. Możemy ją zainstalować w dowolnym projekcie za pomocą composera (komenda composer require phpunit/phpunit). Następnie piszemy testy dziedzicząc po klasie dostarczonej przez paczkę oraz uruchamiamy testy za pomocą programu phpunit który paczka udostępnia.

Laravel ma wbudowane wsparcie dla PHPUnit. W pliku composer.json znajdziemy w polu require-dev paczkę phpunit/phpunit. W głównym katalogu aplikacji znajduje się plik phpunit.xml, w którym trzymane są ustawienia dla PHPUnit. Mamy też katalog tests, który jest miejscem na nasze testy. Zobaczmy przykładowo plik tests/Unit/ExampleTest.php:

// tests/Unit/ExampleTest.php

namespace Tests\Unit;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        $this->assertTrue(true);
    }
}

Jak widzimy test automatyczny to nic innego jak zwykła klasa PHP. Dziedziczy po klasie Tests\TestCase, która dziedziczy po klasie Illuminate\Foundation\Testing\TestCase. Ta klasa z kolei dziedziczy po klasie PHPUnit\Framework\TestCase. Kluczowa jest właśnie ta ostatnia, to dzięki niej program phpunit będzie wiedział, że ma rozpatrywać metody w niej zawarte jako testy. Dwie pierwsze to nakładki Laravela na tę klasę, które ułatwiają testowanie aplikacji postawionych na frameworku.

Każda metoda w tej klasie zaczynająca się od słowa test będzie traktowana jako test, który program phpunit ma wykonać. Jak uruchomić program phpunit? Wystarczy będąc w głównym katalogu aplikacji wpisać w terminal:

vendor/bin/phpunit

Powinniśmy zobaczyć w terminalu coś takiego:

terminal

Program uruchomił wszystkie testy z katalogu tests. W świeżej aplikacji Laravela mamy dwie przykładowe klasy z testami. Wspomnianą wcześniej tests/Unit/ExampleTest.php oraz tests/Feature/ExampleTest.php. Każda z nich zawiera po jednym teście. W wyniku komendy phpunit widzimy tekst OK (2 tests, 2 assertions), który oznacza tyle, że zostały uruchomione dwa testy. Oba wykonały się poprawie, innymi słowy przeszły.

Poświęćmy chwilę na przeanalizowanie kodu w pliku tests/Unit/ExampleTest.php. Widzimy tam linijkę:

$this->assertTrue(true);

Częścią każdego testu jest asercja. W dużym skrócie, asercje służą do sprawdzenia zwracanej wartości. To właśnie asercja decyduje o tym, czy test przeszedł. Dziedzicząc po klasach TestCase mamy do dyspozycji mnóstwo asercji. W tym przykładzie używamy metody assertTrue(), która sprawdza czy podana jako parametr wartość jest prawdziwa. Zamieńmy linijkę na:

$this->assertTrue(false);

Gdy uruchomimy ponownie testy za pomocą komendy vendor/bin/phpunit powinniśmy zobaczyć w terminalu coś takiego:

terminal

Widzimy na czerwonym tle napis: Tests: 2, Assertions: 2, Failures: 1.. Zostały wykonane dwa testy zawierające dwie asercje. Wartość false nie jest oczywiście prawdziwa, w związku z czym test znajdujący się w pliku tests/Unit/ExampleTest.php nie przeszedł.

Teraz przeanalizujmy zawartość pliku tests/Feature/ExampleTest.php:

// tests/Feature/ExampleTest.php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

W metodzie testBasicTest() używamy metody $this->get('/') i przypisujemy jej wartość do zmiennej $response. Metoda get() symuluje wykonanie żądania HTTP GET pod adres wskazany jako pierwszy parametr, w tym przypadku / czyli strona główna naszej aplikacji. Pod zmienną $response znajdzie się obiekt klasy TestResponse, na którym możemy następnie wykonać assercje. W tym przypadku używamy metody assertStatus(200), żeby sprawdzić czy aplikacja zwróciła odpowiedź o statusie HTTP równym 200. To może brzmieć nieco skomplikowanie, ale obiecuję, że temat rozjaśni się gdy zaczniemy pisać własne testy.

Pierwszy test naszej aplikacji

Gdy chcemy tworzyć aplikacje za pomocą techniki TDD najtrudniej jest zdecydować od czego zacząć. Jaki powinien być pierwszy test? W naszej aplikacji będzie kilka mniejszych i większych funkcjonalności. Chcemy by na liście wpisów były widoczne tylko te oznaczone jako opublikowane. Oprócz tego w aplikacji musi się znaleźć formularz dodawania i edytowania wpisu. Do tych formularzy będziemy chcieli dodać walidację. Musi się również znaleźć kod, który będzie blokował użytkownikom możliwość edytowania nie swoich wpisów. Administrator ma za to mieć możliwość usuwania dowolnego wpisu.

Warto zastanowić się jaka jest najbardziej kluczowa, podstawowa funkcjonalność naszej aplikacji. Wydaje mi się, że w aplikacji bloga, będzie to wyświetlanie pojedynczego wpisu. Od tego zatem zacznijmy. W celu ułatwienia wejścia w temat, na razie pominiemy sprawdzenie czy wpis został oznaczony jako opublikowany. Napiszemy test funkcjonalny (ang. feature test), który sprawdzi że po wejściu na adres /posts/1 widzimy na stronie tytuł wpisu o id równym 1.

Stwórzmy w katalogu tests/Feature plik PostsTest.php:

// tests/Feature/PostsTest.php

namespace Tests\Feature;

use Tests\TestCase;

class PostsTest extends TestCase
{
}

Następnie rozpiszmy sobie co ma się znaleźć w naszym pierwszym teście:

// tests/Feature/PostsTest.php

namespace Tests\Feature;

use Tests\TestCase;

class PostsTest extends TestCase
{
    /** @test */
    public function the_posts_show_route_can_be_accessed()
    {
        // Arrange
        // Dodajemy do bazy danych wpis

        // Act
        // Wykonujemy zapytanie pod adres wpisu

        // Assert
        // Sprawdzamy że w odpowiedzi znajduje się tytuł wpisu
    }
}

Kod w teście powinien zazwyczaj zawierać trzy sekcje. W pierwszej sekcji "Arrange" przygotowujemy to co będzie nam potrzebne w systemie, żeby móc wykonać kod w następnych sekcjach. Innymi słowy przygotowujemy sobie system, który będzie testowany. W tym teście będziemy chcieli sprawdzić, że można odwiedzić stronę ze wpisem. Dlatego w sekcji "Arrange" dodamy wpis do bazy danych. W drugiej sekcji "Act" wykonujemy akcję. W naszym teście będzie to wykonanie zapytania HTTP GET pod adres wpisu dodanego w sekcji "Arrange". W ostatniej sekcji "Assert" wykonujemy asercje, czyli sprawdzenia. W naszym teście sprawdzimy, że w odpowiedzi na zapytanie HTTP wykonane w sekcji "Act" znajduje się tytuł wpisu dodanego wcześniej w sekcji "Arrange".

Pisałem wcześniej, że program phpunit traktuje jako testy metody poprzedzone słowem "test". Ja osobiście lubię nazywać testy jak najbardziej "po ludzku", dlatego dodawanie tego prefixu "test" nie zawsze jest mi po drodze. Na szczęście inną możliwością przekazania programowi phpunit, że ma traktować metodę jako test jest poprzedzenie jej adnotacją @test.

W porządku, napiszmy kod testu:

// tests/Feature/PostsTest.php

namespace Tests\Feature;

use App\Post;
use Tests\TestCase;

class PostsTest extends TestCase
{
    /** @test */
    public function the_posts_show_route_can_be_accessed()
    {
        // Arrange
        // Dodajmy do bazy danych wpis
        $post = Post::create([
            'title' => 'Wrabiał krowę w morderstwo cioci',
        ]);

        // Act
        // Wykonajmy zapytanie pod adres wpisu
        $response = $this->get('/posts/' . $post->id);

        // Assert
        // Sprawdźmy że w odpowiedzi znajduje się tytuł wpisu
        $response->assertStatus(200)
            ->assertSeeText('Wrabiał krowę w morderstwo cioci');
    }
}

W pierwszej sekcji używamy Eloquenta, żeby dodać wpis o konkretnym tytule do bazy danych. Może to wydawać się dziwne, bo przecież nie stworzyliśmy jeszcze modelu Post ani migracji tabeli posts. Pamiętajmy jednak, że stosujemy technikę TDD, czyli najpierw test, a dopiero potem implementacja. W drugiej sekcji wykonujemy zapytanie HTTP GET pod adres '/posts' . $post->id i zapisujemy odpowiedź do zmiennej $response. Ponownie, nie mamy jeszcze w aplikacji ścieżki, która obsługuje ten adres. Zaraz ją dodamy. W trzeciej sekcji wykonujemy sprawdzenia. Najpierw używamy asercji assertStatus(), żeby sprawdzić czy zapytanie HTTP zwróciło status 200, to znaczy czy wykonało się poprawnie. Następnie używamy asercji assertSeeText(), która sprawdzi czy w treści odpowiedzi na wykonane zapytanie HTTP znajduje się tytuł naszego wpisu.

Uruchommy nasz test. Możemy podać programowi phpunit ścieżkę do konkretnego pliku, dzięki temu wykonają się tylko testy z tego pliku:

vendor/bin/phpunit tests/Feature/PostsTest.php

Powinniśmy zobaczyć coś takiego:

terminal

Composer nie był w stanie załadować pliku modelu, którego oczekiwał pod ścieżką app/Post.php. Wygenerujemy zatem nasz model:

php artisan make:model Post

I uruchommy test ponownie. Jak można było się spodziewać, test nadal nie przeszedł. Ale pojawił nam się nowy błąd. To dobry znak, oznacza że idziemy do przodu. Test wyrzucił wyjątek:

Illuminate\Database\Eloquent\MassAssignmentException: Add [title] to fillable property to allow mass assignment on [App\Post].

Nie dodaliśmy pola title do pola fillable w naszym modelu i wyrzuciło nam wyjątek MassAssignmentException. Poprawmy to w modelu Post:

// app/Post.php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = ['title'];
}

Uruchommy test ponownie. Powinniśmy zobaczyć następujący błąd:

Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 no such table: posts

To by się zgadzało, stworzyliśmy plik modelu, ale nie stworzyliśmy migracji tabeli posts. Wygenerujmy ją za pomocą artisana:

php artisan make:migration create_posts_table --create=posts

Teraz mamy już plik migracji tabeli posts i jeśli wykonamy komendę php artisan migrate powinniśmy móc uruchomić testy i zobaczyć nowy błąd:

Illuminate\Database\QueryException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'title' in 'field list'

Stworzyliśmy plik migracji, ale nie dodaliśmy kolumny title. Poprawmy to, dodajmy w pliku migracji tabeli posts linijkę: $table->string('title'); i wykonajmy komendę php artisan migrate:fresh, żeby utworzyć na nowo tabele w bazie danych. Następnie uruchommy ponownie test. Powinniśmy zobaczyć nowy błąd:

Expected status code 200 but received 404.

Wygląda na to, że udało nam się dodać wpis do bazy danych. Czyli błędy związane z sekcją "Arrange" mamy już za sobą. Teraz pojawił nam się błąd 404. W sekcji "Act" wykonujemy zapytanie GET pod adres wpisu, a następnie w sekcji "Assert" sprawdzamy czy zapytanie zwróciło status 200. Sprawdzenie nie przechodzi, bo nie zdefiniowaliśmy jeszcze ścieżki. Poprawmy to w pliku routes/web.php:

// routes/web.php

Route::get('/posts/{post}', 'PostController@show');

oraz dodajmy kontroler:

// app/Http/Controllers/PostController.php

namespace App\Http\Controllers;

use App\Post;

class PostController extends Controller
{
    public function show(Post $post)
    {
    }
}

Gdy uruchomimy testy powinniśmy zobaczyć nowy błąd:

Failed asserting that '' contains "Wrabiał krowę w morderstwo cioci".

Wygląda na to, że doszliśmy do ostatniej asercji. Test zwraca nam informację, że pusty string '' nie zawiera spodziewanego tytułu wpisu. Ma to sens, gdyż nie zwracamy nic w metodzie show() kontrolera. Zwróćmy w niej widok:

public function show(Post $post)
{
    return view('posts.show');
}

Po uruchomieniu testów dostajemy następujący błąd:

Expected status code 200 but received 500.

Serwer zwrócił błąd 500. Powodów dla błędu 500 może być wiele. W naszym przypadku powodem jest brak pliku widoku posts/show.blade.php. Dlaczego test nam tego nie podpowiada? Funkcja globalna view() wyrzuca wyjątek jeśli nie odnajdzie pliku widoku. W Laravelu wyjątki obsługiwane są przez klasę App\Exceptions\Handler, która taki wyjątek z funkcji view() zamienia na błąd 500. Na szczęście dla potrzeb testów możemy tą obsługę wyjątków tymczasowo wyłączyć. Wystarczy na początku testu wywołać następującą metodę:

public function the_posts_show_route_can_be_accessed()
{
    $this->withoutExceptionHandling();
    
    // reszta kodu testu
}

Gdy teraz uruchomimy testy, zobaczymy nowy błąd:

InvalidArgumentException: View [posts.show] not found.

Teraz już wiemy co musimy zrobić, dodajmy pusty plik resources/views/posts/show.blade.php, usuńmy z testów linijkę z wywołaniem metody withoutExceptionHandling() i uruchommy ponownie testy:

Failed asserting that '' contains "Wrabiał krowę w morderstwo cioci".

Wróciliśmy do poprzedniego błędu. A to dlatego, że zwracamy pusty widok. Poprawmy to:

// resources/views/posts/show.blade.php

<h1>{{ $post->title }}</h1>

Po uruchomieniu testów znowu dostajemy błąd 500:

Expected status code 200 but received 500.

Dodajmy ponownie na początku test wywołanie metody withoutExceptionHandling() i uruchommy test ponownie. Powinniśmy zobaczyć następujący błąd:

Facade\Ignition\Exceptions\ViewException: Undefined variable: post

W widoku używamy zmiennej $post, której nie przekazaliśmy w kontrolerze. Poprawmy to:

public function show(Post $post)
{
    return view('posts.show')->with('post', $post);
}

Usuńmy z testów linijkę z wywołaniem metody withoutExceptionHandling() i uruchommy ponownie test:

terminal

Sukces! Napisaliśmy pierwszy test dla naszej aplikacji i napisaliśmy kod który implementuje funkcjonalność opisaną w teście. Wiemy, że wchodząc pod adres /posts/1 zobaczymy na ekranie tytuł wpisu o id równym 1. Żeby być tego pewnym, nie musieliśmy nawet włączać przeglądarki.

Kod napisany w tym wpisie znajdziecie na GitHubie.

W tej pierwszej części serii wpisów z tematu "Wprowadzenie do testowania w Laravelu" pominęliśmy dodanie do wpisu pola body, w którym będziemy chcieli przechowywać zawartość wpisu. Pominęliśmy też temat relacji pomiędzy modelem Post a modelem User. Wreszcie, pominęliśmy sprawdzenie czy wpis został oznaczony jako opublikowany. Wszystko to po to, żeby wejście w temat testowania było jak najłatwiejsze. Tymi tematami zajmiemy się w części drugiej.

Wygodny hosting zapewnia duet DigitalOceanLaravel Forge.
Copyright © laravelpolska.com

Drogi Użytkowniku!

Dalsze korzystanie z serwisu bez zmiany ustawień dotyczących cookies w przeglądarce oznacza akceptację plików cookies, co będzie skutkowało zapisywaniem ich na Twoich urządzeniach przez serwis internetowy laravelpolska.com. Jeśli nie wyrażasz zgody na przyjmowanie cookies, prosimy o zmianę ustawień w przeglądarce lub o opuszczenie serwisu. więcej

Stosujemy pliki cookies (tzw. ciasteczka) i inne pokrewne technologie, które mają na celu:

  • dostosowanie zawartości stron internetowych Serwisu do Twoich preferencji oraz optymalizacji korzystania ze stron internetowych; w szczególności pliki te pozwalają rozpoznać Twoje urządzenie i odpowiednio wyświetlić stronę internetową, dostosowaną do Twoich indywidualnych potrzeb;
  • utrzymanie Twojej sesji w Serwisie (po zalogowaniu), dzięki czemu nie musisz na każdej podstronie Serwisu ponownie wpisywać loginu i hasła,
  • zapewnienie bezpieczeństwa podczas korzystania z Serwisu,
  • ulepszenie świadczonych przez nas usług poprzez wykorzystanie danych w celach analitycznych i statystycznych,
  • poznanie Twoich preferencji na podstawie sposobu korzystania z naszych serwisów.

Wykorzystanie cookies pozwala nam zapewnić maksymalną wygodę przy korzystaniu z naszego Serwisu poprzez zapamiętanie Waszych preferencji i ustawień na naszych stronach. Więcej informacji o zamieszczanych plikach cookie oraz o możliwości zmiany ustawień przeglądarki oraz polityce przetwarzania danych znajdziesz w polityce prywatności.

Masz możliwość samodzielnej zmiany ustawień dotyczących cookies w swojej przeglądarce internetowej. Z poziomu przeglądarki internetowej, z której korzystasz, możliwe jest zarządzanie plikami cookies. W najpopularniejszych przeglądarkach istnieje m.in. możliwość:

  • zaakceptowania obsługi cookies, co pozwala na pełne korzystanie z opcji oferowanych przez witryny internetowe;
  • zarządzania plikami cookies na poziomie pojedynczych, wybranych przez użytkownika witryn;
  • określania ustawień dla różnych typów plików cookies, na przykład akceptowania plików stałych, jako sesyjnych itp.;
  • blokowania lub usuwania cookies.

Akceptuję pliki cookies