Всем привет, меня зовут Игорь, я PHP-разработчик в компании Binariks. В этой статье я расскажу вам о возможностях тестирования, которые предоставляет фреймворк Laravel в сочетании с PHPUnit, поэтому запаривайте чаек и готовьтесь к лонгриду.
Наличие валидных тестов с хорошим покрытием — одно из правил качественного кода. С их помощью можно быстро выявить проблемы в функционале, соответственно и ускорить выход функционала в прод. Они упрощают жизнь команды QA, уменьшая количество однотипного мануального тестирования, тем самым уменьшая влияние человеческого фактора на функционал.
В зависимости от степени изоляции тесты разделяют на следующие типы:
- Unit Tests: максимально изолированные тесты. Используется для тестирования отдельного метода или функции. Любые внешние зависимости изолируются на уровне метода или функции.
- Components/Service Tests: тесты, проверяющие работу и взаимодействие отдельных компонентов или сервисов. Изолируются в пределах тестируемого функционала.
- API/HTTP Tests: тесты, проверяющие, как работают отдельные API и работа всех вызываемых при выполнении запроса компонентов.
- Gui Tests: проверяют работу, фронт-энд и бэк-энд части в комплексе.
Как следует проводить тесты
Перед тем как начать рассказ о возможностях фреймворка, думаю стоит напомнить правила хороших тестов.
- Тесты должны быть быстрыми.
- Следует избегать избыточной абстракции в тестах. Код, написанный в тесте, должен быть читаемым и понятным без избыточного копания в коде.
- Названия тестов должны быть читабельными.
- Test-Driven Development (TDD) — это методология, когда тесты пишутся перед имплементированием определенного функционала. Преимущества этого подхода в том, что вы будете сразу писать будущий код таким образом, чтобы его можно было легко тестировать. Эта методология также уменьшает количество регрессивных тестов (тесты, покрывающие функционал после его имплементации).
- Подготовь, проведи, проверь.
Хороший тест имеет три стадии:
- Подготовка — генерирование входных данных и состояний системы.
- Проведение теста — проведение действий, необходимых для теста.
- Проверка — проведение действий для проверки исходных данных и состояний системы.
- Тест должен быть воспроизводимым и возвращать одинаковый результат вне зависимости от количества вызовов теста.
- Тесты не должны зависеть друг от друга. Если у него будет зависимость от другого теста, он может вернуть ошибку в случае отдельного вызова.
- Тест должен проверять только один конкретный тестовый сценарий.
- Входные данные тестов должны быть реалистичными (использовать фейкеры, для генерирования тестовых данных хорошая практика).
- Если для тестирования требуется использовать базу данных, создайте отдельную базу данных, на которой вы будете проводить тесты.
- Избегайте логических выражений в тестах. На рисунке ниже покажу примеры того, как НЕ СТОИТ ДЕЛАТЬ.
- Все внешние зависимости в тесте должны быть изолированы.
- Всегда проверяйте данные соответствующими методами. Проверка данных должна производиться не только по значению, но и по типу.
- Для максимальной пользы тестов интегрируйте ваши тесты в CI/CD — это поможет вам избежать человеческого фактора, который всегда присутствует в разработке.
- Покрытие тестами должно быть максимально возможным. Пишите тесты для разных сценариев. Идеально, когда все возможные варианты работы функционала покрыты тестами. Хорошим показателем считается когда хотя бы 70-80% функционала покрыто тестами.
- Помимо всего выше перечисленного, тесты могут служить примерами того, как работает тестируемый функционал. Поэтому хорошо рассматривать тесты как часть спецификации или документации.
Особенности тестирования в Laravel с помощью PHPUnit
Поддержка тестирования с помощью PHPUnit включена из коробки, а файл phpunit.xml уже настроен для вашей программы. Также во фреймворк добавлено много вспомогательных методов, которые удобны и упрощают тестирование.
По умолчанию каталог tests в Laravel содержит две папки Feature и Unit.
Unit
Unit-тесты предназначены для тестирования небольшой изолированной части вашего кода. Отдельный метод класса или функции. Тесты в директории Unit не инициируют ваше Laravel-приложение, поэтому с юнит-тестами вы не сможете получить доступ к сервисам Laravel или базе данных.
Приведу пример написания юнит-тестов с практическим применением вышеперечисленных правил тестов.
Задание: имплементировать метод getSubscription в классе SubscriptionService, который будет предлагать пользователю подписки в зависимости от типа его аккаунта.
- Если у пользователя есть премиум-аккаунт — предлагать ему премиум-подписки.
- Если у пользователя обычный аккаунт — предлагать ему базовые подписки.
Согласно методологии TDD начнем с написания тестов и описываем ожидаемое поведение метода.
Создаем сам функционал:
Вызываем тесты:
Unit-тесты полезны для проверки работы важнейших частей кода. Они быстры и пишутся относительно просто, дают высокую стабильность коду, покрытому тестами.
Однако их изолированность имеет и недостатки, а именно они не могут гарантировать корректное взаимодействие всех отдельно протестированных частей кода, при реальных сценариях, когда код не изолирован.
Feature
В отличие от директории Unit, тесты в каталоге Feature предназначены для тестирования взаимодействия разных компонентов программы. Они инициируют ваше Laravel приложение. Соответственно, с помощью этих тестов можно проверять большую часть вашего кода, начиная от отдельных методов, работающих в инфраструктуре Laravel, то, как несколько объектов взаимодействуют друг с другом или даже полный запрос HTTP, включая ответ с сервера.
Согласно рекомендациям разработчиков фреймворка Laravel, большинство ваших тестов должно быть Feature-тестами. Потому что эти типы тестов обеспечивают большую уверенность в том, что ваша система работает должным образом.
Написание Feature-тестов имеет несколько особенностей.
- Поскольку тесты имеют доступ к базе данных, для тестирования следует создать отдельную базу данных, где будут генерироваться и тестироваться данные.
- PHPunit.xml файл позволит задать или перезаписать все .env переменные (в том числе и соответствующие конфигурации). Кроме того, вы можете создать файл .env.testing в таком случае он будет использоваться вместо .env файла при тестировании.
- Для обновления данных Laravel предлагает трейт Illuminate Foundation Testing RefreshDatabase, который осуществляет все миграции и инициирует транзакцию, которая вернет вашу базу данных в исходное состояние после завершения теста.
- Для генерирования тестовых данных следует использовать Faker, Factories, Seeders.
- При тестировании HTTP-запросов следует использовать функционал Named Routes — это простой и удобный способ генерирования сложных URL.
- Используйте Laravel Mocks для логинов, загрузки файлов, Events и т.д.
Тестирование компонента. Пример: в модель User необходимо добавить scope, который будет фильтровать записи в базе данных по полю is_admin.
- – Если в scope будет передаваться true – возвращать все записи, в которых поле is_admin true;
- – Если в scope будет передаваться false – возвращать все записи, в которых поле is_admin false.
Напишем тест:
Добавим подходящий scope в модель User:
Убедимся, что тест работает корректно:
Добавим тест для второго случая:
Проверим оба случая:
Тестирование АРИ. Пример:
- Написать тест, который будет проверять следующий функционал.
- Создать endpoint POST: api/articles.
- Этот endpoint должен храниться в базе данных (user_id (id залогованного пользователя), title, text).
- Доступ к endpoint может иметь только пользователь с правами admin (users.is_admin===true).
- При успешном выполнении ответ с сервера статус 200 и последующие поля.
{
“user_id”: (id пользователя),
«id»: (article id),
«title»: (поле title)
}
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class CreateArticleTest extends TestCase
{
use RefreshDatabase;
use WithFaker;
public function setUp(): void
{
parent::setUp();
// исключаем из теста все мидлверы, не относящиеся к тесту
$this->withoutMiddleware();
}
public function testCreateArticleSuccess()
{
// Создаем пользователя-админа
$user = User::factory()->create([
‘is_admin’ => true,
]);
// С помощью фейкера генерируем данные запроса
$request = [
‘title’ => $this->faker()->text(6),
‘text’ => $this->faker()->text(50),
];
// С помощью метода actingAs() имитируем поведение запроса для админа
$response = $this->actingAs($user)
// выполняем сам запрос
->post(
// первый параметр – роут сгенерированный с помощью Laravel функционала Named routes
route(‘articles.create’),
// Второй параметр – request body
$request,
);
// Проверка результата:
// Проверяем, ответ ли с сервера 200
$response->assertStatus(200)
//Проверка структуры ответа
->assertJsonStructure([
‘id’,
‘user_id’,
‘title’,
])
// проверка достоверности полученных данных
->assertJson([
‘user_id’ => $user->id,
‘title’ => $request[‘title’],
])
// Конвертируем ответ в array, чтобы получить id статьи
->json();
// Проверяем, была ли создана запись в базе данных
$this->assertDatabaseHas(‘articles’, [
‘id’ => $response[‘id’],
‘user_id’ => $response[‘user_id’],
‘title’ => $response[‘title’],
‘text’ => $request[‘text’],
]);
}
}
Laravel «из коробки» имеет множество методов, которые будут полезны при тестировании. Кратко пройдусь по ним и добавлю полезные ссылки (на документацию :-)):
Название говорит само за себя — их цель провести тестирование конкретных эндпойнтов сервера. Часто при тестировании приходится сталкиваться со следующими методами фреймворка:
- withoutMiddleware() — помогает отключить все заданные Middleware;
- withHeaders(), withSession(), withCookies() — имитируют наличие в запросе определенных хедеров, сессий, куки;
- actingAs() — имитирует поведение сервера при определенном авторизованном пользователе;
- get(), post(), put(), patch(), delete(), json() — имитирует соответствующие методы вызова севера;
- assertStatus(), assertJson(), assertJsonStructure() — предоставляют возможность проверки статус ответа с сервера и их структуры;
- view(), blade() — возможность проверки сгенерированных фреймворком страниц.
Методы для имитации работы функционала:
Во фреймворке предусмотрена возможность имитирования очередей, загрузок файлов, нотификаций, передачи в контейнер имитации работы определенного объекта, работы со временем.
Тестирование базы данных:
Контроль за миграциями, возможность отмены изменений в базе данных после окончания теста, методы для тестирования наличия (или отсутствия) определенных данных в базе данных.
Методы для тестирования консоли:
Фреймворк предоставляет возможности тестирования входных и выходных данных артисановской консоли.
При установке пекеджа Dusk появляется возможность имитирования работы браузеров и написания GUI тестов.
В этой статье я постарался кратко описать общие хорошие практики, которые следует использовать при написании тестов, и показать примеры разных типов тестов и их имплементации в среде Laravel, сделать краткий обзор возможностей тестирования в среде фреймворка.
Надеюсь, эта статья будет вам полезна. Всем успехов.