Wprowadzenie do testowania w Laravelu - część 4

Paweł Mysior
3 grudnia 2019

Kontynuujemy serię wpisów, w której budujemy wspólnie aplikację bloga pisząc do niej testy automatyczne przy użyciu techniki Test Driven Development. W trzeciej części napisaliśmy test sprawdzający, że wpis dodany przez formularz znalazł się bazie danych. Dowiedzieliśmy się jak zalogować użytkownika podczas testowania oraz napisaliśmy test sprawdzający, że niezalogowani użytkownicy nie mogą dodawać wpisów.

W tej części dodamy walidację do naszego formularza tworzenia wpisu pisząc testy, dla każdego pola. Dowiemy się też jak użyć metody setUp() w celu automatycznego zalogowania użytkownika, przed każdym testem.

Walidacja pola title

Zanim napiszemy pierwszy test dla funkcjonalności walidacji, zastanówmy się jakie mają być reguły walidacji dla formularza dodawania wpisu. W formularzu znajdą się trzy pola odpowiadające kolumnom w tabeli posts: published_at, title, body. W pierwszym wpisie tej serii ustaliliśmy reguły walidacji dotyczące tytułu i zawartości: tytuł wpisu musi być unikatowy, zawartość wpisu może być pusta, ale jeśli ma wartość to musi mieć minimum 3 znaki. Dla pola published_at przyjmiemy następując reguły: pole może być puste, ale jeśli nie jest puste to musi być datą w konkretnym formacie.

Zacznijmy od napisania pierwszego, prostego testu dla pola title. Zanim będziemy sprawdzać czy tytuł jest unikatowy, sprawdzimy czy w formularzu podano jakąkolwiek wartość dla tego pola:

// tests/Feature/PostsTest.php

/** @test */
public function the_title_field_is_required()
{
    $user = factory(User::class)->create();

    $response = $this->actingAs($user)->post('/posts', [
        'title' => null,
    ]);

    $response->assertSessionHasErrors('title');
}

Ja pamiętamy z trzeciej części, wpisy mogą dodawać tylko zalogowani użytkownicy, dlatego w pierwszej linijce tworzymy użytkownika. Następnie logujemy tego użytkownika i wysyłamy na adres /posts formularz z pustą wartością (null) w polu title. Na koniec wykonujemy asercję assertSessionHasErrors() na zmiennej $response. Ta asercja sprawdza czy dla pola title znajdują się w sesji jakieś błędy walidacji. Dodamy za moment regułę walidacji, która sprawi, że przesłanie pustego pola z tytułem wpisu właśnie to spowoduje.

Gdy uruchomimy test powinniśmy zobaczyć:

Session is missing expected key [errors].
Failed asserting that false is true.

Mechanizm walidacji Laravela dodaje błędy do sesji pod kluczem errors. Obecnie w kontrolerze nie mamy w ogóle walidacji, dlatego nie ma takiego klucza w sesji. Dodajmy w kontrolerze walidację dla pola title:

// app/Http/Controllers/PostController.php

public function store(Request $request)
{
    $request->validate([
        'title' => 'required',
    ]);

    $request->user()->posts()->create($request->only([
        'published_at',
        'title',
        'body',
    ]));
}

Teraz test powinien przejść. Napiszmy zatem następny, sprawdzający że tytuł wpisu jest unikatowy:

// tests/Feature/PostsTest.php

/** @test */
public function the_title_field_must_be_unique()
{
    $user = factory(User::class)->create();
    $existingPost = factory(Post::class)->create([
        'title' => 'Wrabiał krowę w morderstwo cioci',
    ]);

    $response = $this->actingAs($user)->post('/posts', [
        'title' => 'Wrabiał krowę w morderstwo cioci',
    ]);

    $response->assertSessionHasErrors('title');
}

Tym razem w sekcji "arrange" oprócz stworzenia użytkownika tworzymy w bazie danych wpis o tytule "Wrabiał krowę w morderstwo cioci". Następnie próbujemy przesłać formularz z takim samym tytułem. Rezultatem powinno być pojawienie się w sesji błędu dla pola title i właśnie to sprawdzamy w asercji. Gdy uruchomimy test, znów zobaczymy błąd:

Session is missing expected key [errors].
Failed asserting that false is true.

Formularz przeszedł walidację, chociaż nie powinien. Dodajmy zatem regułę unique do naszego kontrolera:

// app/Http/Controllers/PostController.php

use Illuminate\Validation\Rule;

public function store(Request $request)
{
    $request->validate([
        'title' => [
            'required',
            Rule::unique('posts'),
        ],
    ]);

    $request->user()->posts()->create($request->only([
        'published_at',
        'title',
        'body',
    ]));
}

Gdy teraz uruchomimy test, powinien przejść. Drobna uwaga: w sekcji "arrange" tworzymy wpis i przypisujemy go do zmiennej $existingPost, której potem nie wykorzystujemy. W kodzie należy unikać takich sytuacji, a dobry edytor powinien nam to podpowiedzieć. Przy pisaniu testów istotne jest jednak wyraźne opisanie tego co sprawdza dany test. Dzięki przypisaniu tego wpis do dobrze nazwanej zmiennej w jasny sposób wyrażamy po co go tam tworzymy. To w moim przekonaniu zwiększa czytelność testu.

Automatyczne logowanie użytkownika przed testem

Zanim przejdziemy do pisania testów dla kolejnego pola, zwróćmy uwagę na to, że na początku obu testów musieliśmy stworzyć i następnie zalogować użytkownika. Tak samo będzie w przypadku następnych testów walidacji. Mamy tu dwa problemy. Po pierwsze, musimy pisać tą samą, powtarzalną instrukcję na początku, każdego testu. Po drugie, z punktu widzenia testu sprawdzającego, że pusta wartość pola title nie przechodzi walidacji, krok polegający na stworzeniu i logowaniu użytkownika jest niepotrzebnym "hałasem". To nie jest kluczowe w tym teście, a jest wymagane tylko ze względu na to, że użytkownik musi być zalogowany, żeby dodać wpis. Ten test miał dokumentować, że pole title musi mieć niepustą wartość, a zaczynamy go od stworzenia użytkownika. To sprawia, że test jest mniej czytelny. Innymi słowy, niepotrzebnie zakłóca przekaz testu.

Co możemy w takiej sytuacji zrobić? Możemy przenieść instrukcję tworzenia i logowania użytkownika poza ten test. Jak dowiedzieliśmy się w części drugiej tej serii wpisów, PHPUnit pozwala nam dodać kod, który wykona się przed każdym testem. Wykorzystamy tę możliwość wydzielając testy, które wymagają zalogowania użytkownika do oddzielnej klasy.

Stwórzmy sobie katalog tests/Feature/Authenticated, w którym będziemy trzymać testy wymagające zalogowania użytkownika. Następnie dodajmy w nim abstrakcyjną klasę AuthenticatedTestCase, po której wszystkie klasy w tym katalogu będą dziedziczyć:

// tests/Feature/Authenticated/AuthenticatedTestCase.php

namespace Tests\Feature\Authenticated;

use App\User;
use Tests\TestCase;

abstract class AuthenticatedTestCase extends TestCase
{
    protected $user;

    protected function setUp(): void
    {
        parent::setUp();

        $this->user = factory(User::class)->create();

        $this->actingAs($this->user);
    }
}

Nadpisujemy w niej metodę setUp(), która jest wywoływana przed każdym testem przez PHPUnit. W pierwszej kolejności trzeba pamiętać, żeby wywołać metodę setUp() rodzica, czyli klasy z której już dziedziczymy, poprzez instrukcję parent::setUp(). Następnie tworzymy nowego użytkownika i przypisujemy go do pola user w klasie. Na końcu logujemy tego użytkownika za pomocą metody actingAs(). Wszystkie testy w klasach dziedziczącej po tej będą miały zalogowanego użytkownika. Stwórzmy teraz nową klasę w katalogu Authenticated i przenieśmy tam test sprawdzający dodawanie wpisu napisany w poprzednim wpisie oraz testy walidacji, które napisaliśmy przed momentem:

// tests/Feature/Authenticated/PostsTest.php

namespace Tests\Feature\Authenticated;

use App\Post;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

class PostsTest extends AuthenticatedTestCase
{
    use RefreshDatabase;

    /** @test */
    public function a_post_can_be_created()
    {
        $user = factory(User::class)->create();

        $this->actingAs($user)->post('/posts', [
            'published_at' => '2019-11-19 12:00:00',
            'title' => 'Odebrał żelazko zamiast telefonu',
            'body' => 'Miał pomóc żonie, a skończyło się tragedią.',
        ]);

        $this->assertDatabaseHas('posts', [
            'user_id' => $user->id,
            'published_at' => '2019-11-19 12:00:00',
            'title' => 'Odebrał żelazko zamiast telefonu',
            'body' => 'Miał pomóc żonie, a skończyło się tragedią.',
        ]);
    }

    /** @test */
    public function the_title_field_is_required()
    {
        $user = factory(User::class)->create();

        $response = $this->actingAs($user)->post('/posts', [
            'title' => null,
        ]);

        $response->assertSessionHasErrors('title');
    }

    /** @test */
    public function the_title_field_must_be_unique()
    {
        $user = factory(User::class)->create();
        $existingPost = factory(Post::class)->create([
            'title' => 'Wrabiał krowę w morderstwo cioci',
        ]);

        $response = $this->actingAs($user)->post('/posts', [
            'title' => 'Wrabiał krowę w morderstwo cioci',
        ]);

        $response->assertSessionHasErrors('title');
    }
}

Dzięki temu, że klasa dziedziczy po klasie AuthenticatedTestCase możemy usunąć w tych testach instrukcje odpowiedzialne za tworzenie i logowanie użytkownika:

// tests/Feature/Authenticated/PostsTest.php

namespace Tests\Feature\Authenticated;

use App\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;

class PostsTest extends AuthenticatedTestCase
{
    use RefreshDatabase;

    /** @test */
    public function a_post_can_be_created()
    {
        $this->post('/posts', [
            'published_at' => '2019-11-19 12:00:00',
            'title' => 'Odebrał żelazko zamiast telefonu',
            'body' => 'Miał pomóc żonie, a skończyło się tragedią.',
        ]);

        $this->assertDatabaseHas('posts', [
            'user_id' => $this->user->id,
            'published_at' => '2019-11-19 12:00:00',
            'title' => 'Odebrał żelazko zamiast telefonu',
            'body' => 'Miał pomóc żonie, a skończyło się tragedią.',
        ]);
    }

    /** @test */
    public function the_title_field_is_required()
    {
        $response = $this->post('/posts', [
            'title' => null,
        ]);

        $response->assertSessionHasErrors('title');
    }

    /** @test */
    public function the_title_field_must_be_unique()
    {
        $existingPost = factory(Post::class)->create([
            'title' => 'Wrabiał krowę w morderstwo cioci',
        ]);

        $response = $this->post('/posts', [
            'title' => 'Wrabiał krowę w morderstwo cioci',
        ]);

        $response->assertSessionHasErrors('title');
    }
}

Po tych zmianach testy są czytelniejsze.

Walidacja pól body oraz published_at

Dodajmy zatem w tej klasie kolejne testy, tym razem sprawdzające walidację pola body. Zgodnie z ustaleniami, zawartość wpisu może być pusta, ale jeśli ma wartość to musi mieć minimum 3 znaki. Napiszmy test sprawdzający, że pusta wartość pola body nie powoduje błędu walidacji:

/** @test */
public function the_body_is_not_required()
{
    $response = $this->post('/posts', [
        'body' => null,
    ]);

    $response->assertSessionDoesntHaveErrors('body');
}

Używamy tutaj asercji assertSessionDoesntHaveErrors(), która sprawdza że dla danego pola nie ma błędów walidacji. Jako, że nie mamy reguł walidacji dla pola body, ten test powinien przejść bez modyfikowania kodu kontrolera. Dodajmy drugi test, w którym spróbujemy dodać zbyt krótką wartość w polu body:

/** @test */
public function the_body_must_be_at_least_3_characters()
{
    $response = $this->post('/posts', [
        'body' => 'aa',
    ]);

    $response->assertSessionHasErrors('body');
}

Ten test nie przejdzie, dodajmy zatem w kontrolerze regułę walidacji:

// app/Http/Controllers/PostController.php

public function store(Request $request)
{
    $request->validate([
        'title' => [
            'required',
            Rule::unique('posts'),
        ],
        'body' => 'min:3',
    ]);
    
    ...
}

Gdy uruchomimy test, powinien przejść, ale gdy uruchomimy wszystkie testy zobaczymy, że test the_body_is_not_required nie przechodzi. Reguła walidacji min:3 sprawdza czy podany string ma minimum 3 znaki. Jeśli podamy pustą wartość (null), reguła nie przejdzie. Jeśli chcemy, żeby pod kątem długości były sprawdzane tylko niepuste wartości to musimy dodać modyfikator nullable do listy reguł pola body:

// app/Http/Controllers/PostController.php

public function store(Request $request)
{
    $request->validate([
        'title' => [
            'required',
            Rule::unique('posts'),
        ],
        'body' => [
            'nullable',
            'min:3',
        ],
    ]);
    
    ...
}

Teraz wszystkie testy powinny przejść. Testy dla reguł walidacji pola published_at będą analogiczne do testów dla pola body. Dodajmy je jednak dla porządku:

// tests/Feature/Authenticated/PostsTest.php

/** @test */
public function the_published_at_is_not_required()
{
    $response = $this->post('/posts', [
        'published_at' => null,
    ]);

    $response->assertSessionDoesntHaveErrors('published_at');
}

/** @test */
public function the_published_at_must_be_a_valid_date()
{
    $response = $this->post('/posts', [
        'published_at' => 'NOT-A-DATE-STRING',
    ]);

    $response->assertSessionHasErrors('published_at');
}

Oraz reguły walidacji dla tego pola:

// app/Http/Controllers/PostController.php

public function store(Request $request)
{
    $request->validate([
        'title' => [
            'required',
            Rule::unique('posts'),
        ],
        'body' => [
            'nullable',
            'min:3',
        ],
        'published_at' => [
            'nullable',
            'date',
        ],
    ]);
    
    ...
}

W czwartej części serii wpisów z tematu "Wprowadzenie do testowania w Laravelu" napisaliśmy testy sprawdzające walidację pól formularza dodawania wpisu. Przy okazji dowiedzieliśmy się jak użyć metody setUp(), żeby automatycznie logować użytkownika przed każdym testem.

Kod napisany w tym wpisie znajdziecie na GitHubie.

W następnej części dodamy możliwość edytowania oraz usuwania wpisu.

Jeśli macie jakiekolwiek pytania dotyczące wpisu, zapraszamy do komentarzy poniżej lub na naszego Slacka!

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