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.
assertDatabaseHas
Zapytanie typu POST oraz 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!