Всем привет, меня зовут Игорь, я PHP-разработчик в компании Binariks. В этой статье я расскажу вам о возможностях тестирования, которые предоставляет фреймворк Laravel в сочетании с PHPUnit, поэтому запарьте чаек и готовьтесь к лонгриду 🙂
Наличие валидных тестов с хорошим покрытием — одно из правил качественного кода. С их помощью можно быстро выявить проблемы в функционале и ускорить выход на прод. Они упрощают жизнь команды QA, уменьшая количество однотипного мануального тестирования, тем самым уменьшая влияние человеческого фактора.
В зависимости от степени изоляции тесты разделяют на следующие типы:
- Unit Tests: максимально изолированные тесты. Используются для тестирования отдельного метода или функции. Любые внешние зависимости изолируются на уровне метода или функции.
- Components/Service Tests: тесты, проверяющие работу и взаимодействие отдельных компонентов или сервисов. Изолируются в пределах тестируемого функционала.
- API/HTTP Tests: тесты, проверяющие, как работают отдельные АРI и работу всех вызываемых при выполнении запроса компонентов.
- Gui Tests: проверяют работу, фронтенд- и бэкенд-части в комплексе.
Как нужно проводить тесты
Перед тем, как начать рассказ о возможностях фреймворка, думаю, стоит напомнить правила хороших тестов:
- Тесты должны быть быстрыми.
- Следует избегать избыточной абстракции в тестах. Код должен быть читаемым и понятным без излишнего копания.
- Названия тестов должны быть читабельными.
- Test-Driven Development (TDD) — это методология, когда тесты пишутся перед имплементированием определенного функционала. Преимущества этого подхода в том, что вы сразу будете писать будущий код так, чтобы его можно было легко тестировать. Эта методология также уменьшает количество регрессивных тестов (тесты, покрывающие функционал после его имплементации).
- Подготовь, проведи, проверь.
У хорошего теста три стадии:
- подготовка — генерирование входящих данных и состояний системы;
- проведение теста — проведение действий, необходимых для теста;
- проверка — проведение действий по проверке исходных данных и состояний системы.
- Тест должен быть воспроизводимым и возвращать одинаковый результат вне зависимости от количества вызовов.
- Тесты не должны зависеть друг от друга. Если у одного теста будет зависимость от другого, он может вернуть ошибку при отдельном вызове.
- Тест должен проверять только один конкретный тестовый сценарий.
- Входящие данные тестов должны быть реалистичными (использовать фейкеры для генерирования тестовых данных — хорошая практика).
- Если для тестирования нужно использовать базу данных, создайте отдельную, на которой вы будете проводить тесты.
- Избегайте логических выражений в тестах. На рисунке ниже покажу примеры того, как ДЕЛАТЬ НЕ СТОИТ:
- Все внешние зависимости в тесте должны быть изолированы.
- Всегда проверяйте данные соответствующими методами. Проверка данных должна производиться не только по значению, но и по типу.
- Для максимальной пользы тестов интегрируйте ваши тесты в CI/CD — это поможет вам избежать человеческого фактора, который всегда присутствует в разработке.
- Покрытие тестами должно быть максимально возможным. Пишите тесты для разных сценариев. Идеально, когда все возможные варианты работы функционала покрыты тестами. Хорошим показателем считается, когда покрыто хотя бы 70-80% функционала.
- Помимо всего перечисленного выше, тесты могут служить примерами того, как работает тестируемый функционал. Поэтому хорошо рассматривать тесты как часть спецификации или документации.
Особенности тестирования в Laravel с помощью PHPUnit
Поддержка тестирования с помощью PHPUnit включена «из коробки», а файл phpunit.xml
уже сконфигурирован для вашей программы. Также во фреймворк добавлено много вспомогательных методов, которые удобны и упрощают тестирование.
По умолчанию каталог tests
в Laravel содержит две папки Feature и Unit.
Unit
Юнит-тесты предназначены для тестирования небольшой изолированной части вашего кода. Отдельный метод класса или функции.
Тесты в директории Unit не инициируют ваше Laravel-приложение, поэтому с юнит-тестами вы не сможете получить доступ к сервисам Laravel или базе данных.
Приведу пример написания юнит-тестов с практическим применением вышеперечисленных правил.
Задание: имплементировать метод getSubscription
в классе SubscriptionService
, который будет предлагать пользователю подписки в зависимости от типа его аккаунта:
- если у пользователя есть премиум-аккаунт — предлагать ему премиум-подписки;
- если у пользователя обычный аккаунт — предлагать ему базовые подписки.
Согласно методологии TDD, начнем с написания тестов и описываем ожидаемое поведение метода:
Создаем сам функционал:
Вызываем тесты:
Юнит-тесты полезны для проверки работы отдельных важных элементов кода. Они быстры и относительно просто пишутся, дают высокую стабильность коду, покрытому тестами.
Однако у их изолированности есть и недостатки, а именно — они не могут гарантировать корректное взаимодействие всех отдельно протестированных частей кода при реальных сценариях, когда код не изолирован.
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:
Убедимся, что тест работает корректно:
Добавим тест для второго случая:
Проверим оба случая:
Тестирование АРI. Пример:
- написать тест, который будет проверять следующий функционал;
- создать 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 «из коробки» есть много методов, которые будут полезны при тестировании. Кратко пройдусь по ним и добавлю полезные ссылки (на документацию 🙂 ):
1) HTTP-тесты. Название говорит само за себя — их цель — провести тестирование конкретных эндпойнтов сервера. Часто при тестировании приходится сталкиваться со следующими методами фреймворка:
withoutMiddleware()
— помогает отключить все или заданныеMiddleware
;withHeaders()
,withSession()
,withCookies()
— имитируют наличие в запросе определенных хедеров, сессий, куки;actingAs()
— имитирует поведение сервера при определенном авторизованном пользователе;get()
,post()
,put()
,patch()
,delete()
,json()
— имитирует соответствующие методы вызова севера;assertStatus()
,assertJson()
,assertJsonStructure()
— предоставляют возможность проверки статуса ответа с сервера и его структуры;view()
,blade()
— возможность проверки сгенерированных фреймворком страниц.
2) Методы для имитации работы функционала:
- во фреймворке предусмотрена возможность имитирования очередей, загрузок файлов, нотификаций, передачи в контейнер имитации работы определенного объекта, работы со временем.
- контроль за миграциями, возможность отмены изменений в базе данных после окончания теста, методы для тестирования наличия (или отсутствия) определенных данных в базе данных.
4) Методы для тестирования консоли:
- фреймворк предоставляет возможности тестирования входных и выходных данных артисановской консоли;
- с установкой пекеджа Dusk появляется возможность имитирования работы браузеров и написания GUI-тестов.
В этой статье я пытался кратко описать общие хорошие практики, которые следует использовать при написании тестов, и показать примеры различных типов тестов и их имплементации в среде Laravel, сделать краткий обзор возможностей тестирования в среде фреймворка.
Надеюсь, эта статья будет вам полезна. Всем удачи!
Читайте также: Как написать одностраничное приложение на Laravel и Vue.js за 45 минут
Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: