• Czas czytania ~11 min
  • 16.06.2022

Jeśli mówimy o testach automatycznych lub testach jednostkowych w dowolnym języku programowania, mamy do czynienia z dwiema grupami osób:

  • Those who don't write automated tests and think they're a waste of time
  • Those who do write tests and then can't imagine their work without them

W tym artykule postaram się przekonać były obóz do spojrzenia na drugą stronę i zobaczenia korzyści oraz zobaczenia, jak łatwo jest rozpocząć automatyczne testowanie w Laravel.

Najpierw porozmawiajmy o „dlaczego”, a następnie pokażę kilka bardzo podstawowych przykładów „jak”.


Dlaczego potrzebujesz testów automatycznych

Testy automatyczne nie są skomplikowane: po prostu uruchamiają dla Ciebie części kodu i zgłaszają wszelkie błędy. To najprostszy sposób na ich opisanie.Wyobraź sobie, że uruchamiasz nową funkcję w swojej aplikacji, a następnie osobisty asystent robota idzie i ręcznie testuje nową funkcjonalność za Ciebie, sprawdzając również, czy nowy kod nie zepsuł niczego ze starych funkcji.

To główna korzyść: automatyczne ponowne testowanie wszystkich funkcji.I może się wydawać, że to dodatkowa praca, ale jeśli nie każesz temu „robotowi”, żeby to zrobił, to powinieneś zrobić to ręcznie, prawda? A może po prostu uruchamiasz nowe funkcje bez zbytniego testowania, mając nadzieję, że użytkownicy zgłoszą błędy? Nazywam tę metodę sarkastycznie „rozwojem opartym na skrzyżowaniu palców”.

Z każdą nową funkcją Twojej aplikacji testy automatyczne przynoszą coraz większe korzyści.

  • Feature 1: saves X minutes of testing manually
  • Feature 2: saves 2X minutes - for feature 2 and feature 1 again
  • Feature 3: saves 3X minutes...
  • etc.

Masz pomysł. Wyobraź sobie swoją aplikację za rok lub dwa, z nowymi programistami w zespole, którzy nawet nie wiedzieliby, jak działa ta „Funkcja 1” ani jak ją odtworzyć do testów. Tak więc twoje przyszłe ja będzie ci ogromnie dziękować za napisanie automatycznych testów.

Oczywiście, jeśli uważasz, że Twój projekt jest bardzo krótkoterminowy i nie zależy Ci zbytnio na jego przyszłości...Nie, wierzę w Twoje dobre intencje, więc pokażę Ci, jak łatwo rozpocząć testowanie.


Nasze pierwsze testy automatyczne

Aby uruchomić pierwszy automatyczny test w Laravel, nie musisz pisać żadnego kodu. Tak, dobrze to przeczytałeś. Wszystko jest już skonfigurowane i przygotowane w domyślnej instalacji Laravela, łącznie z pierwszym prawdziwym przykładem podstawowym.

Możesz spróbować zainstalować projekt Laravel i natychmiast uruchomić pierwsze testy:

laravel new project
cd project
php artisan test

To powinien być wynik w Twojej konsoli:

PASS  Tests\Unit\ExampleTest
✓ that true is true
 
 PASS  Tests\Feature\ExampleTest
✓ the application returns a successful response
 
Tests:  2 passed
Time:   0.10s

Jeśli spojrzymy na domyślny folder /tests Laravela, mamy dwa pliki.

testy/cecha/przykładowy test.php:

class ExampleTest extends TestCase
{
    public function test_the_application_returns_a_successful_response()
    {
        $response = $this->get('/');
 
        $response->assertStatus(200);
    }
}

Nie trzeba znać żadnej składni, aby zrozumieć, co się tutaj dzieje: ładowanie strony głównej i sprawdzanie, czy kod stanu HTTP to „200 OK”.

Zauważ również, że nazwa metody test_the_application_returns_a_successful_response() staje się czytelnym tekstem podczas przeglądania wyników testu, po prostu zastępując symbol podkreślenia spacją.

testy/jednostka/przykładowyTest.php:

class ExampleTest extends TestCase
{
    public function test_that_true_is_true()
    {
        $this->assertTrue(true);
    }
}

Wygląda to trochę bez sensu, sprawdzanie, czy prawda to prawda? Nieco później omówimy konkretnie testy jednostkowe. Na razie musisz zrozumieć, co zwykle dzieje się w każdym teście.

  • Each test file in the tests/ folder is a PHP Class extending the TestCase of PHPUnit
  • Inside of each class, you may create multiple methods, usually one method for one situation to be tested
  • Inside of each method, there are three actions: preparation of the situation, then action, and then checking (asserting) if the result is as expected

Strukturalnie to wszystko, co musisz wiedzieć, wszystko inne zależy od tego, co dokładnie chcesz przetestować.

Aby wygenerować pustą klasę testową, po prostu uruchom to polecenie:

php artisan make:test HomepageTest

Wygeneruje plik tests/Feature/HomepageTest.php:

class HomepageTest extends TestCase
{
    // Replace this method with your own ones
    public function test_example()
    {
        $response = $this->get('/');
 
        $response->assertStatus(200);
    }
}

Co, jeśli testy się nie powiodą?

Pozwólcie, że pokażę, co się stanie, jeśli asercje testowe nie zwrócą oczekiwanego wyniku.

Zmodyfikujmy przykładowe testy do tego:

class ExampleTest extends TestCase
{
    public function test_the_application_returns_a_successful_response()
    {
        $response = $this->get('/non-existing-url');
 
        $response->assertStatus(200);
    }
}
 
 
class ExampleTest extends TestCase
{
    public function test_that_true_is_false()
    {
        $this->assertTrue(false);
    }
}

A teraz, jeśli ponownie uruchomimy test rzemieślniczy PHP:

 
 FAIL  Tests\Unit\ExampleTest
⨯ that true is true
 
 FAIL  Tests\Feature\ExampleTest
⨯ the application returns a successful response
 
---
 
• Tests\Unit\ExampleTest > that true is true
Failed asserting that false is true.
 
at tests/Unit/ExampleTest.php:16
   12▕      * @return void
   13▕      */
   14▕     public function test_that_true_is_true()
   15▕     {
➜  16▕         $this->assertTrue(false);
   17▕     }
   18▕ }
   19▕
 
• Tests\Feature\ExampleTest > the application returns a successful response
Expected response status code [200] but received 404.
Failed asserting that 200 is identical to 404.
 
at tests/Feature/ExampleTest.php:19
   15▕     public function test_the_application_returns_a_successful_response()
   16▕     {
   17▕         $response = $this->get('/non-existing-url');
   18▕
➜  19▕         $response->assertStatus(200);
   20▕     }
   21▕ }
   22▕
 
 
Tests:  2 failed
Time:   0.11s

Jak widać, istnieją dwa stwierdzenia oznaczone jako FAIL, z objaśnieniami poniżej i strzałkami do dokładnej linii testowej, która nie powiodła się. A więc tak są pokazane błędy. Wygodne, prawda?


Prosty przykład z życia wzięty: formularz rejestracyjny

Bądźmy bardziej praktyczni i spójrzmy na przykład z życia wzięty. Wyobraź sobie, że masz formularz i musisz przetestować różne przypadki: sprawdź, czy nie powiedzie się, jeśli jest wypełniony nieprawidłowymi danymi, sprawdź, czy udaje się z poprawnymi danymi wejściowymi itp.

Czy wiesz, że oficjalny Zestaw startowy Laravel Breeze zawiera testy funkcji wewnątrz? Spójrzmy więc na kilka przykładów stamtąd:

testy/funkcja/test rejestracji.php

use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
 
class RegistrationTest extends TestCase
{
    use RefreshDatabase;
 
    public function test_registration_screen_can_be_rendered()
    {
        $response = $this->get('/register');
 
        $response->assertStatus(200);
    }
 
    public function test_new_users_can_register()
    {
        $response = $this->post('/register', [
            'name' => 'Test User',
            'email' => '[email protected]',
            'password' => 'password',
            'password_confirmation' => 'password',
        ]);
 
        $this->assertAuthenticated();
        $response->assertRedirect(RouteServiceProvider::HOME);
    }
}

Tutaj mamy dwa testy w jednej klasie, ponieważ oba są związane z formularzem rejestracyjnym: jeden sprawdza, czy formularz jest załadowany poprawnie, a drugi sprawdza, czy przesyłanie działa poprawnie.

Zapoznaliśmy się z dwiema innymi metodami sprawdzania wyniku, dwoma dodatkowymi asercjami: $this->assertAuthenticated() i $response->assertRedirect() .Możesz sprawdzić wszystkie dostępne asercje w oficjalnej dokumentacji PHPUnit i Odpowiedź Laravel.Pamiętaj, że niektóre ogólne asercje mają miejsce na obiekcie $this, podczas gdy inne sprawdzają konkretną $response z wywołania trasy.

Kolejną ważną rzeczą jest instrukcja use RefreshDatabase; z cechą zawartą powyżej klasy.Jest to potrzebne, gdy działania testowe mogą mieć wpływ na bazę danych, tak jak w tym przykładzie, rejestracja dodaje nowy wpis w tabeli bazy danych users. W tym celu będziesz musiał stworzyć oddzielną testową bazę danych, która będzie odświeżana za pomocą php artisan migrate:fresh za każdym razem, gdy testy są wykonywane.

Masz dwie opcje: fizycznie utwórz oddzielną bazę danych lub użyj bazy danych SQLite w pamięci. Oba są skonfigurowane w pliku phpunit.xml, który jest domyślnie dostarczany z Laravel. W szczególności potrzebujesz tej części:

<php>
    <env name="APP_ENV" value="testing"/>
    <env name="BCRYPT_ROUNDS" value="4"/>
    <env name="CACHE_DRIVER" value="array"/>
    <!-- <env name="DB_CONNECTION" value="sqlite"/> -->
    <!-- <env name="DB_DATABASE" value=":memory:"/> -->
    <env name="MAIL_MAILER" value="array"/>
    <env name="QUEUE_CONNECTION" value="sync"/>
    <env name="SESSION_DRIVER" value="array"/>
    <env name="TELESCOPE_ENABLED" value="false"/>
</php>

Zobacz DB_CONNECTION i DB_DATABASE, które są zakomentowane?Jeśli masz SQLite na swoim serwerze, najłatwiejszą czynnością jest po prostu odkomentowanie tych linii, a twoje testy będą uruchamiane na tej bazie danych w pamięci.

W tym teście zapewniamy, że użytkownik został pomyślnie uwierzytelniony i przekierowany na właściwą stronę główną, ale możemy również przetestować rzeczywiste dane w bazie danych.

Oprócz tego kodu:

$this->assertAuthenticated();
$response->assertRedirect(RouteServiceProvider::HOME);

Możemy również użyć Asercje testowania bazy danych i wykonaj coś takiego:

$this->assertDatabaseCount('users', 1);
 
// Or...
$this->assertDatabaseHas('users', [
    'email' => '[email protected]',
]);

Inny przykład z życia: formularz logowania

Rzućmy okiem na jeszcze jeden test Laravela Breeze.

testy/Feature/AuthenticationTest.php:

class AuthenticationTest extends TestCase
{
    use RefreshDatabase;
 
    public function test_login_screen_can_be_rendered()
    {
        $response = $this->get('/login');
 
        $response->assertStatus(200);
    }
 
    public function test_users_can_authenticate_using_the_login_screen()
    {
        $user = User::factory()->create();
 
        $response = $this->post('/login', [
            'email' => $user->email,
            'password' => 'password',
        ]);
 
        $this->assertAuthenticated();
        $response->assertRedirect(RouteServiceProvider::HOME);
    }
 
    public function test_users_can_not_authenticate_with_invalid_password()
    {
        $user = User::factory()->create();
 
        $this->post('/login', [
            'email' => $user->email,
            'password' => 'wrong-password',
        ]);
 
        $this->assertGuest();
    }
}

Tu chodzi o formularz logowania. Logika jest podobna do rejestracji, prawda?Ale trzy metody zamiast dwóch, więc jest to przykład testowania zarówno dobrych, jak i złych scenariuszy. Tak więc wspólną logiką jest to, że powinieneś przetestować oba przypadki: kiedy wszystko idzie dobrze, a kiedy kończy się niepowodzeniem.

Ponadto w tym teście widać użycie Fabryki baz danych: Laravel tworzy fałszywego użytkownika (ponownie w odświeżonej testowej bazie danych), a następnie próbuje się zalogować z poprawnym lub nieprawidłowe dane uwierzytelniające.

Ponownie, Laravel generuje domyślną fabrykę z fałszywymi danymi dla modelu Użytkownik, po wyjęciu z pudełka.

baza danych/fabryki/fabryka użytkownika.php:

class UserFactory extends Factory
{
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
        ];
    }
}

Widzisz, ile rzeczy przygotowuje sam Laravel, abyśmy mogli łatwo rozpocząć testy?

Więc, jeśli uruchomimy test rzemieślniczy PHP po zainstalowaniu Laravela Breeze, powinniśmy zobaczyć coś takiego:

PASS  Tests\Unit\ExampleTest
✓ that true is true
 
 PASS  Tests\Feature\Auth\AuthenticationTest
✓ login screen can be rendered
✓ users can authenticate using the login screen
✓ users can not authenticate with invalid password
 
 PASS  Tests\Feature\Auth\EmailVerificationTest
✓ email verification screen can be rendered
✓ email can be verified
✓ email is not verified with invalid hash
 
 PASS  Tests\Feature\Auth\PasswordConfirmationTest
✓ confirm password screen can be rendered
✓ password can be confirmed
✓ password is not confirmed with invalid password
 
 PASS  Tests\Feature\Auth\PasswordResetTest
✓ reset password link screen can be rendered
✓ reset password link can be requested
✓ reset password screen can be rendered
✓ password can be reset with valid token
 
 PASS  Tests\Feature\Auth\RegistrationTest
✓ registration screen can be rendered
✓ new users can register
 
 PASS  Tests\Feature\ExampleTest
✓ the application returns a successful response
 
Tests:  17 passed
Time:   0.61s

Testy funkcji vs testy jednostkowe vs inne

Widziałeś podfoldery testy/funkcja i testy/jednostka.Jaka jest różnica między nimi? Odpowiedź jest trochę „filozoficzna”.

Globalnie, poza ekosystemem Laravel/PHP, istnieją różne rodzaje testów automatycznych. Możesz znaleźć takie terminy jak:

  • Unit tests
  • Feature tests
  • Integration tests
  • Functional tests
  • End-to-end tests
  • Acceptance tests
  • Smoke tests
  • etc.

Brzmi to skomplikowanie, a rzeczywiste różnice między tymi typami testów czasami są zamazane.

Z mojego osobistego doświadczenia z projektami Laravela, bezwzględna większość testów to testy funkcji, a nie testy jednostkowe. Najpierw musisz sprawdzić, czy Twoja aplikacja działa, w jaki sposób używaliby jej prawdziwi ludzie.

Następnie, jeśli masz specjalne obliczenia lub logikę, które możesz zdefiniować jako jednostkę z parametrami, możesz utworzyć testy jednostkowe specjalnie do tego celu.

Czasami pisanie testów wymaga zmiany samego kodu i refaktoryzacji, aby był bardziej „testowalny”: rozdzielenie jednostek na specjalne klasy lub metody.

php artisan make:test OrderPriceTest --unit

Kiedy/jak przeprowadzać testy?

class OrderPriceTest extends TestCase
{
    public function test_example()
    {
        $this->assertTrue(true);
    }
}

Jakie jest faktyczne użycie tego testu rzemieślniczego PHP, kiedy należy go uruchomić?

Istnieją różne podejścia, w zależności od przepływu pracy Twojej firmy, ale zazwyczaj musisz upewnić się, że wszystkie testy są „zielone” (czyli bez błędów), zanim prześlesz najnowsze zmiany kodu do repozytorium.

Tak więc pracujesz lokalnie nad swoim zadaniem, a kiedy czujesz, że skończyłeś, uruchamiasz testy, aby upewnić się, że niczego nie złamałeś.Pamiętaj, że Twój kod może powodować błędy nie tylko w Twojej logice, ale także nieumyślnie łamać inne zachowanie w kodzie innej osoby napisanym dawno temu.

class OrderPriceService
{
    public function calculatePrice($productId, $quantity, $tax = 0.0)
    {
    	// Some kind of calculation logic
    }
}

Jeśli pójdziemy o krok dalej, możemy zautomatyzować wiele rzeczy.Za pomocą różnych narzędzi CI/CD możesz określić, które testy mają być wykonywane za każdym razem, gdy ktoś wypchnie zmiany do określonej gałęzi Git lub przed scaleniem kodu z gałęzią produkcyjną. Najłatwiejszym przepływem pracy byłoby użycie akcji Github, mam osobny film demonstrujący to.

class OrderPriceTest extends TestCase
{
    public function test_single_product_no_taxes()
    {
    	$product = Product::factory()->create(); // generate a fake product
    	$price = (new OrderPriceService())->calculatePrice($product->id, 1);
        $this->assertEquals(1, $price);
    }
 
    public function test_single_product_with_taxes()
    {
    	$price = (new OrderPriceService())->calculatePrice($product->id, 1, 20);
        $this->assertEquals(1.2, $price);
    }
 
    // More cases with more parameters
}

Co należy przetestować?

Istnieją różne opinie na temat tego, jak duży powinien być Twój tak zwany „pokrycie testów”: czy powinieneś testować każdą operację i każdy możliwy przypadek na każdej stronie, czy po prostu ograniczyć swoją pracę do najważniejszych części.

Rzeczywiście w tym miejscu zgadzam się z ludźmi, którzy obwiniają automatyczne testy za to, że zajmują więcej czasu niż przynoszą rzeczywiste korzyści.Może się tak zdarzyć, jeśli napiszesz testy dla każdego szczegółu. To powiedziawszy, może to być wymagane przez Twój projekt: główne pytanie brzmi „jaka jest cena potencjalnego błędu”.


Innymi słowy, musisz nadać priorytet swoim wysiłkom testowym za pomocą pytania „Co by się stało, gdyby ten kod się nie powiódł?” Jeśli Twój system płatności zawiera błędy, wpłynie to bezpośrednio na biznes.Następnie, jeśli funkcje ról/uprawnień są zepsute, jest to ogromny problem z bezpieczeństwem.

Podoba mi się to, jak Matt Stauffer wyraził to podczas jednej z konferencji: „Najpierw musisz przetestować te rzeczy, które, jeśli się nie powiedzą, wyrzucą Cię z pracy”. Oczywiście to przesada, ale masz pomysł: najpierw przetestuj ważne rzeczy. A potem inna funkcjonalność, jeśli masz na to czas.

PEST: nowa popularna alternatywa dla PHPUnit

Wszystkie powyższe przykłady są oparte na domyślnym narzędziu testowym Laravel: PHPUnit. Ale z biegiem lat w ekosystemie pojawiały się inne narzędzia, a jednym z najnowszych popularnych jest PEST. Stworzony przez oficjalnego pracownika Laravela Nuno Maduro, ma na celu uproszczenie składni, dzięki czemu pisanie kodu do testów jest jeszcze szybsze.

Pod maską działa na PHPUnit, jako dodatkowa warstwa, próbując tylko zminimalizować niektóre domyślne powtarzające się części kodu PHPUnit.


Przyjrzyjmy się przykładowi. Pamiętasz domyślną klasę testu Cech w Laravel?Przypomnę:

Czy wiesz, jak wyglądałby ten sam test z PEST?

Tak, JEDNA linia kodu i to wszystko.


namespace Tests\Feature;
 
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
 
class ExampleTest extends TestCase
{
    public function test_the_application_returns_a_successful_response()
    {
        $response = $this->get('/');
 
        $response->assertStatus(200);
    }
}
test('the application returns a successful response')->get('/')->assertStatus(200);
  • Creating classes and methods for everything;
  • Extending TestCase;
  • Putting actions on separate lines - in PEST, you can chain them.
php artisan make:test HomepageTest --pest

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297