• Время чтения ~12 мин
  • 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

Если мы посмотрим на папку Laravel /tests по умолчанию, у нас будет два файла.

tests/Feature/ExampleTest.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 поставляется с функциональные тесты внутри? Итак, давайте взглянем на несколько примеров оттуда:

tests/Feature/RegistrationTest.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 автоматически создает фабрику по умолчанию с поддельными данными для модели User.

база данных/фабрики/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

Функциональные тесты VS Модульные тесты VS Другие

Вы видели вложенные папки tests/Feature и tests/Unit.В чем разница между ними? Ответ несколько «философский».

Во всем мире за пределами экосистемы Laravel/PHP существуют различные виды автоматических тестов. Вы можете найти такие термины, как:

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

Звучит сложно, а фактические различия между этими типами тестов иногда размыты.

По моему личному опыту работы с проектами Laravel, абсолютное большинство тестов — это Feature, а не модульные тесты. Во-первых, вам нужно проверить, работает ли ваше приложение, как его будут использовать реальные люди.

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

Иногда для написания тестов требуется изменение самого кода и его рефакторинг, чтобы сделать его более "пригодным для тестирования": разделение модулей на специальные классы или методы.

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 Nuno Maduro, его целью является упрощение синтаксиса, позволяющее еще быстрее писать код для тестов.

Под капотом он работает поверх 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