• Час читання ~11 хв
  • 16.06.2022

Говорячи про автоматизовані або модульні тести будь-якою мовою програмування, є дві групи людей:

  • 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

Отож, за допомогою цієї статті я спробую переконати колишній табір поглянути з іншого боку та побачити переваги, а також побачити, як легко почати з автоматичного тестування в Laravel.

Спочатку давайте поговоримо про "чому", а потім я покажу кілька дуже простих прикладів "як".


Навіщо потрібні автоматичні тести

Автоматичні тести не є складними: вони просто запускають частини вашого коду для вас і повідомляють про будь-які помилки. Це найпростіший спосіб їх описати.Уявіть, що ви запускаєте нову функцію у своєму додатку, а потім персональний робот-помічник вручну перевірить для вас нову функцію, а також перевірить, чи не порушує новий код старі функції.

Це головна перевага: повторне тестування всіх функцій автоматично.І це може здатися додатковою роботою, але якщо ви не наказуєте цьому «роботу» це робити, то ви повинні робити це вручну, чи не так? Або ви просто запускаєте нові функції без зайвого тестування, сподіваючись, що користувачі повідомлять про помилки? Я саркастично називаю цей метод "розробкою, керованою схрещеними пальцями".

З кожною новою функцією вашої програми автоматизовані тести окупаються все більше й більше.

  • 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.

Ви зрозуміли ідею. Уявіть свою програму через рік-два з новими розробниками в команді, які навіть не знають, як працює ця «Функція 1» або як відтворити її для тестування. Отже, ваше майбутнє «я» буде дуже вдячний вам за написання автоматизованих тестів.

Звичайно, якщо ви думаєте, що ваш проект дуже короткостроковий і вам не дуже важливо його майбутнє...Ні, я вірю у ваші добрі наміри, тому дозвольте мені показати вам, як легко почати тестування.


Наші перші автоматизовані тести

Щоб запустити перший автоматизований тест у Laravel, вам не потрібно писати код. Так, ви правильно прочитали. Все вже налаштовано та підготовлено в стандартній установці Laravel, включаючи перший реальний базовий приклад.

Можна спробувати встановити проект Laravel і негайно запустити перші тести:

laravel new project
cd project
php artisan test

Це має бути результат у вашій консолі:

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

Якщо ми подивимося на папку /tests Laravel за замовчуванням, у нас є два файли.

тести/Функція/ПрикладТест.php:

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

Немає необхідності знати будь-який синтаксис, щоб зрозуміти, що тут відбувається: завантаження домашньої сторінки та перевірка, чи є код статусу HTTP "200 OK".

Також зверніть увагу, як назва методу test_the_application_returns_a_successful_response() стає читабельним текстом під час перегляду результатів тесту, просто замінюючи символ підкреслення пробілом.

tests/Unit/ExampleTest.php:

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

Це виглядає трохи безглуздо, перевірити, чи правда? Конкретно про модульні тести ми поговоримо трохи пізніше. Наразі вам потрібно зрозуміти, що зазвичай відбувається в кожному тесті.

  • 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

З точки зору структури, це все, що вам потрібно знати, все інше залежить від конкретних речей, які ви хочете перевірити.

Щоб створити порожній тестовий клас, просто запустіть цю команду:

php artisan make:test HomepageTest

Це створить файл 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);
    }
}

Що, якщо тести провалилися?

Дозвольте мені показати вам, що станеться, якщо тестові твердження не повернуть очікуваного результату.

Давайте відредагуємо приклади тестів до цього:

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);
    }
}

А тепер, якщо ми знову запустимо php artisan test:

 
 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

Як бачите, є два твердження, позначені як FAIL, з поясненнями нижче та стрілками до точного тестового рядка, який не завершив твердження. Отже, ось як відображаються помилки. Зручно, чи не так?


Простий приклад із реального життя: реєстраційна форма

Давайте практичніше і розглянемо приклад із реального життя. Уявіть, що у вас є форма, і вам потрібно протестувати різні випадки: перевірте, чи не вдається вона, якщо вона заповнена недійсними даними, перевірте, чи успішно вона введена за допомогою правильного введення тощо.

Чи знаєте ви, що офіційний Початковий набір Laravel Breeze поставляється з тести функцій всередині? Отже, давайте подивимося на кілька прикладів звідти:

тести/Функція/РеєстраціяТест.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);
    }
}

Тут у нас є два тести в одному класі, оскільки обидва вони пов’язані з реєстраційною формою: один перевіряє, чи правильно завантажено форму, а інший перевіряє, чи правильно подання.

Ми знайомимося з двома іншими методами перевірки результату, ще двома твердженнями: $this->assertAuthenticated() і $response->assertRedirect() .Ви можете перевірити всі доступні твердження в офіційній документації PHPUnit і Відповідь Laravel.Пам’ятайте, що деякі загальні твердження відбуваються з об’єктом $this, тоді як інші перевіряють конкретний $response з виклику маршруту.

Іншою важливою річчю є оператор use RefreshDatabase; з ознакою, включеною над класом.Це потрібно, коли ваші тестові дії можуть вплинути на базу даних, як у цьому прикладі, реєстрація додає новий запис у таблицю бази даних users. Для цього вам потрібно буде створити окрему базу даних тестування, яка буде оновлюватися за допомогою php artisan migrate:fresh щоразу, коли тести виконуються.

У вас є два варіанти: фізично створити окрему базу даних або використовувати базу даних SQLite в пам’яті. Обидва вони налаштовані у файлі phpunit.xml, який за замовчуванням поставляється разом із Laravel. Зокрема, вам потрібна ця частина:

<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>

Перегляньте DB_CONNECTION і DB_DATABASE, які закоментовані?Якщо на вашому сервері є SQLite, найпростіше просто розкоментувати ці рядки, і ваші тести виконуватимуться в цій базі даних у пам’яті.

У цьому тесті ми стверджуємо, що користувач успішно автентифікований і перенаправлений на правильну домашню сторінку, але ми також можемо перевірити фактичні дані в базі даних.

На додаток до цього коду:

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

Ми також можемо використовувати Тестування баз даних і зробіть щось на кшталт цього:

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

Ще один приклад із реального життя: форма входу

Давайте подивимося ще на один тест від Laravel Breeze.

tests/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();
    }
}

Це стосується форми входу. Логіка схожа на реєстрацію, чи не так?Але три методи замість двох, тому це приклад тестування як хороших, так і поганих сценаріїв. Отже, загальна логіка полягає в тому, що ви повинні перевірити обидва випадки: коли все йде добре, а коли невдало.

Також те, що ви бачите в цьому тесті, – це використання Фабрики баз даних: Laravel створює фальшивого користувача (знову у вашій оновленій тестовій базі даних), а потім намагається ввійти з правильним або неправильні облікові дані.

Знову ж таки, Laravel генерує фабрику за замовчуванням із підробленими даними для моделі Користувач із коробки.

база даних/фабрики/UserFactory.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),
        ];
    }
}

Дивіться, скільки речей підготував сам Laravel, щоб нам було б легко почати тестування?

Отже, якщо ми запустимо php artisan test після встановлення Laravel Breeze, ми повинні побачити щось подібне:

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

Функціональні тести проти модульних тестів проти інших

Ви бачили вкладені папки tests/Feature і tests/Unit.Яка різниця між ними? Відповідь трохи «філософська».

У всьому світі, за межами екосистеми Laravel/PHP, існують різні види автоматизованих тестів. Ви можете знайти такі терміни, як:

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

Звучить складно, а фактичні відмінності між цими типами тестів іноді розмиті.

З мого особистого досвіду роботи з проектами Laravel, абсолютна більшість тестів є тестами функцій, а не модульними тестами. По-перше, вам потрібно перевірити, чи працює ваша програма, як її використовують реальні люди.

Далі, якщо у вас є спеціальні обчислення чи логіка, які ви можете визначити як одиницю з параметрами, ви можете створити модульні тести спеціально для цього.

Іноді написання тестів вимагає зміни самого коду та рефакторингу, щоб він був більш "тестованим": розділення одиниць на спеціальні класи або методи.

php artisan make:test OrderPriceTest --unit

Коли/як запускати тести?

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

Як насправді використовується цей тест php artisan, коли його потрібно запустити?

Існують різні підходи, залежно від робочого процесу вашої компанії, але зазвичай вам потрібно переконатися, що всі тести є "зеленими" (тобто без помилок), перш ніж надсилати останні зміни коду до сховища.

Отже, ви працюєте над своїм завданням локально, а коли відчуваєте, що закінчили, ви запускаєте тести, щоб переконатися, що ви нічого не порушили.Пам’ятайте, що ваш код може спричинити помилки не лише у вашій логіці, але й ненавмисно порушити іншу поведінку в чужому коді, написаному давно.

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

Якщо ми підемо ще далі, то можна автоматизувати багато речей.За допомогою різних інструментів CI/CD ви можете вказати, що ваші тести будуть виконуватися щоразу, коли хтось вносить зміни до певної гілки Git або перед об’єднанням коду в виробничу гілку. Найпростішим робочим процесом було б використання Github Actions, у мене є окреме відео, що демонструє це.

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
}

Що потрібно перевірити?

Існують різні думки щодо того, наскільки великим має бути ваше так зване "охоплення тестами": чи варто тестувати кожну операцію та кожен можливий випадок на кожній сторінці, чи просто обмежувати свою роботу найважливішими частинами.

Дійсно, я згоден з людьми, які звинувачують автоматичне тестування в тому, що воно займає більше часу, ніж приносить реальну користь.Це може статися, якщо ви пишете тести для кожної деталі. Тим не менш, це може знадобитися вашому проекту: головне питання — «яка ціна потенційної помилки».


Іншими словами, вам потрібно визначити пріоритетність своїх зусиль із тестування, задавши питання "Що станеться, якщо цей код не вдасться?" Якщо у вашій платіжній системі є помилки, це безпосередньо вплине на бізнес.Тоді, якщо ваші ролі/дозволи не працюють, це серйозна проблема безпеки.

Мені подобається, як Метт Стауффер сформулював це під час однієї конференції: «Спочатку потрібно перевірити ці речі, які, якщо вони зазнають невдачі, призведуть до звільнення з роботи». Звичайно, це перебільшення, але ви зрозуміли: спочатку перевірте важливі речі. А потім інший функціонал, якщо у вас є на це час.

PEST: Нова популярна альтернатива PHPUnit

Усі наведені вище приклади засновані на стандартному інструменті тестування Laravel: PHPUnit. Але з роками в екосистемі з’являлися інші інструменти, і одним із останніх популярних є PEST. Створено офіційним співробітником Laravel Нуно Мадуро, він має на меті спрощення синтаксису, що робить написання коду для тестів ще швидшим.

Під капотом він працює поверх PHPUnit, як додатковий шар, просто намагаючись мінімізувати деякі повторювані за замовчуванням частини коду PHPUnit.


Давайте розглянемо приклад. Пам’ятаєте тестовий клас Feature за замовчуванням у Laravel?Нагадаю:

Чи знаєте ви, як виглядав би той самий тест із PEST?

Так, ОДИН рядок коду, і все.


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

Про мене

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...

Про автора CrazyBoy49z
WORK EXPERIENCE
Контакти
Ukraine, Lutsk
+380979856297