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.
title
Walidacja pola 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.
body
oraz published_at
Walidacja pól 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!