Wprowadzenie do testowania w Laravelu - część 3

Paweł Mysior
19 listopada 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 drugiej części dowiedzieliśmy jak automatycznie migrować bazę danych, przed wywołaniem testów oraz jak używać w testach fabryk modeli. Napisaliśmy też test sprawdzający, że na liście wpisów wyświetlają się tylko te oznaczone jako opublikowane. Przy okazji omówiliśmy temat różnicy pomiędzy testami funkcjonalnymi a jednostkowymi.

W tym wpisie zajmiemy się tematem testowania czy wpis dodany przez formularz znalazł się bazie danych. Następnie dowiemy się jak zalogować użytkownika w testach i automatycznie powiążemy dodawany wpis z zalogowanym użytkownikiem. Na końcu napiszemy test, w którym sprawdzimy że niezalogowani użytkownicy nie mogą dodawać wpisów.

Zapytanie typu POST oraz assertDatabaseHas

Zaczniemy od sprawdzenia, że po wysłaniu formularza w bazie danych pojawia się wpis z podanymi w formularzu danymi. Pamiętając o technice TDD, zacznijmy od napisania testu:

// tests/Feature/PostsTest.php

/** @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', [
        'published_at' => '2019-11-19 12:00:00',
        'title' => 'Odebrał żelazko zamiast telefonu',
        'body' => 'Miał pomóc żonie, a skończyło się tragedią.',
    ]);
}

W tym teście nie mamy sekcji "arrange", gdyż na razie nie jest nam potrzebna. Przechodzimy od razu do sekcji "act", w której wykonujemy zapytanie HTTP typu POST na adres /posts. Jako drugi parametr metody $this->post() przekazujemy tablicę z danymi formularza. Innymi słowy symulujemy sytuację, w której użytkownik wypełnił i przesłał formularz z podanymi wartościami.

Następnie używamy asercji assertDatabaseHas() do sprawdzenia czy w tabeli posts w bazie danych znajduje się rekord o podanych parametrach.

Gdy wywołamy test powinniśmy zobaczyć mniej więcej coś takiego:

1) Tests\Feature\PostsTest::a_post_can_be_created
Failed asserting that a row in the table [posts] matches the attributes {
    ...
}.

The table is empty.

Dodajmy na początku testu wywołanie $this->withoutExceptionHandling();, żeby sprawdzić na czym polega problem:

Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException: The POST method is not supported for this route. Supported methods: GET, HEAD.

Zgodnie z przyjętą konwencją REST, chcemy użyć tej samej ścieżki /posts do obsługi wyświetlania wpisów oraz do obsługi dodawania wpisu. Router aplikacji podpowiada nam, że znalazł ścieżkę /posts, ale nie wolno nam wykonać na niej zapytania typu POST. Dodajmy ścieżkę obsługującą metodę POST:

// routes/web.php

Route::post('/posts', 'PostController@store');

Oraz wywołajmy test:

BadMethodCallException: Method App\Http\Controllers\PostController::store does not exist.

Dodajmy metodę w kontrolerze:

// app/Http/Controllers/PostController.php

public function store()
{
}

Gdy wywołamy test powinniśmy wrócić do tego co mieliśmy na początku, czyli informacji że tabela w bazie danych jest pusta. Możemy usunąć z kodu testu wywołanie metody withoutExceptionHandling(). Dodajmy w kontrolerze kod, który doda wpis do bazy danych:

public function store(Request $request)
{
    Post::create($request->only([
        'published_at',
        'title',
        'body',
    ]));
}

Teraz nasz test powinien przejść.

Symulowanie zalogowania użytkownika

Chcielibyśmy, żeby wpis był przy dodawaniu automatycznie powiązany z obecnie zalogowanym użytkownikiem. Zmodyfikujmy test, który napisaliśmy wcześniej:

// tests/Feature/PostsTest.php

/** @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ą.',
    ]);
}

W pierwszej linijce testu używamy fabryki modelu User i tworzymy użytkownika. To nasza sekcja "arrange". Następnie używamy metody actingAs(), by zalogować się w aplikacji tym użytkownikiem. Zapytania wykonane do aplikacji po użyciu metody actingAs() będą traktowane tak jakby wykonywał je zalogowany użytkownik. W asercji dodajemy nowy warunek - wpis musi należeć do zalogowanego użytkownika. Gdy wywołamy test, powinniśmy zobaczyć następujący błąd:

Illuminate\Database\QueryException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'user_id' in 'where clause' (SQL: select count(*) as aggregate from `posts` where (`user_id` = 1 and `published_at` = 2019-11-19 12:00:00 and `title` = Odebrał żelazko zamiast telefonu and `body` = Miał pomóc żonie, a skończyło się tragedią.))

Asercja assertDatabaseHas wykonuje zapytanie na tabeli posts dodając każdy element tablicy jako warunek WHERE i zlicza wyniki. To zapytanie wyrzuciło błąd, bo nie mamy w tabeli posts kolumny user_id. Poprawmy to modyfikując plik migracji tabeli posts:

// database/migrations/2019_11_05_074454_create_posts_table.php

public function up()
{
    Schema::create('posts', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->unsignedBigInteger('user_id');
        $table->datetime('published_at')->nullable();
        $table->string('title');
        $table->text('body');
        $table->timestamps();
    });
}

Gdy wywołamy test powinniśmy zobaczyć informację, że tabela jest pusta. Możemy domyślić się dlaczego tak jest. Dodaliśmy w tabeli kolumnę user_id, która nie ma domyślnej wartości i jednocześnie w kodzie kontrolera dodajemy wpis nie podając wartości tej kolumny. Jeśli się nie domyślamy, możemy zawsze pomóc sobie używając metody withoutExceptionHandling():

Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1364 Field 'user_id' doesn't have a default value ...

Dodajmy zatem w kontrolerze kod, który przypisze wpis do zalogowanego użytkownika. Można to zrobić na kilka sposobów, ja wybrałem taki:

// app/Http/Controllers/PostController.php

public function store(Request $request)
{
    $request->user()->posts()->create($request->only([
        'published_at',
        'title',
        'body',
    ]));
}

Wyciągam z klasy Request model zalogowanego użytkownika, używam jego relacji posts() i metody create(), która stworzy wpis uzupełniając za nas wartość kolumny user_id. Muszę jeszcze zdefiniować relację na modelu User:

// app/User.php

public function posts()
{
    return $this->hasMany(Post::class);
}

Gdy uruchomimy nasz test, powinien przejść. Jednak, gdy uruchomimy wszystkie testy za pomocą komendy vendor/bin/phpunit zobaczymy, że zepsuliśmy prawie wszystkie pozostałe testy:

Tests: 10, Assertions: 3, Errors: 7.

Przy wszystkich zaznaczonych na czerwono testach pojawia się ten sam błąd: Field 'user_id' doesn't have a default value. Dodajmy kolumnę user_id do fabryki modelu Post:

// database/factories/PostFactory.php

use App\Post;
use App\User;
use Faker\Generator as Faker;

$factory->define(Post::class, function (Faker $faker) {
    return [
        'user_id' => factory(User::class),
        'title' => $faker->sentence,
        'body' => $faker->paragraph,
    ];
});

Do pola user_id przypisujemy wartość factory(User::class). Jak to działa? Laravel tworząc model Post za pomocą fabryki PostFactory zobaczy, że do pola user_id przypisaliśmy fabrykę modelu User. Stworzy zatem model User za pomocą jego fabryki UserFactory i zwróci w tym miejscu id tego stworzonego modelu. To powinno naprawić wszystkie nasze testy.

Ograniczenie dostępu do ścieżki dla niezalogowanych użytkowników

Zanim napiszemy test, który sprawdzi że wpisy dodawać mogą tylko zalogowani użytkownicy, wygenerujmy sobie ścieżki i widoki do logowania i rejestracji. W wersji 5.* frameworka wystarczy wpisać w terminalu komendę php artisan make:auth . Począwszy od wersji 6.0 widoki związane z uwierzytelnianiem przeniesione zostały do zewnętrznej paczki laravel/ui, którą musimy najpierw zainstalować za pomocą komendy composer require laravel/ui --dev, a następnie wywołać za pomocą komendy php artisan ui vue --auth.

Wiemy, że gdy niezalogowany użytkownik chce wejść na adres dostępny tylko dla zalogowanych użytkowników, Laravel wykonuje przekierowanie pod adres z formularzem logowania. Wykorzystajmy tę wiedzę i napiszmy nasz test:

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

    $response->assertRedirect('/login');
}

Wykonujemy zapytanie typu POST pod adres /posts i zapisujemy jego wynik do zmiennej $response. Pod zmienną $response będzie obiekt klasy TestResponse, na którym następnie wykonujemy asercję assertRedirect(), która sprawdza, że w odpowiedzi na wykonane zapytanie zostaliśmy przekierowani do strony /login.

Po uruchomieniu testu pojawi nam się następujący błąd:

Response status code [500] is not a redirect status code.
Failed asserting that false is true.

Jak zwykle, gdy widzimy w teście błąd 500 to sięgamy po metodę withoutExceptionHandling(). Dodajemy ją w teście i uruchamiamy test ponownie:

Symfony\Component\Debug\Exception\FatalThrowableError: Call to a member function posts() on null

Kod doszedł w kontrolerze do linijki $request->user()->posts()... i wyrzucił błąd. Pod $request->user() spodziewamy się modelu użytkownika, ale dostajemy null. To ma sens, gdyż w tym teście nie logujemy użytkownika. Usuńmy z testu wywołanie withoutExceptionHandling() i wprowadźmy zmiany w naszym pliku routes/web.php:

// routes/web.php

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

Route::middleware('auth')->group(function () {
    Route::post('/posts', 'PostController@store');
});

Teraz test powinien już przejść. Powinny też działać wszystkie nasze pozostałe testy.

W tej trzeciej części serii wpisów z tematu "Wprowadzenie do testowania w Laravelu" napisaliśmy test sprawdzający, że przesłany przez formularz wpis znalazł się w bazie danych. Dowiedzieliśmy się też jak zalogować użytkownika w testach i użyliśmy tego mechanizmu, żeby sprawdzić, że dodawany wpis jest powiązany z użytkownikiem. W końcu, napisaliśmy test sprawdzający, że tylko zalogowani użytkownicy mogą dodawać wpisy.

Kod napisany w tym wpisie znajdziecie na GitHubie.

W następnej części dodamy do funkcjonalności dodawania wpisu walidację pól z formularza. Posłużymy się oczywiście techniką TDD.

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