Простота — один из главных факторов, почему люди выбирает Laravel. Это касается и тестов — фреймворк позволяет их делать проще, быстрее и лучше, в чем я убеждаюсь изо дня в день. Если вы погрузитесь в Laravel-сообщество, то найдете удивительные вещи, такие как Test Driven Laravel от Adam Wathan, серия Testing skill от Jeffrey Way, Confident Laravel и Tests Generator Shift от Jason McCreary.
Лично я большой поклонник подхода Сверху-вниз (Top-down) TDD (Test-Driven Development — Разработка через тестирование), и я подталкиваю многих разработчиков/команд к его использованию, но сам я все еще не до конца понимаю, как организовать свои тесты, что использовать модульные или функциональные тесты, и как нужно писать сами тесты, могу ли я рассматривать тесты в качестве документацию для себя и для других разработчиков?
Последние пару дней я трачу большую часть своего времени на рефакторинг комплекта тестов и открываю для себя интересные приёмы и хитрости, которые помогли мне сделать мои тесты лучше, особенно для пользователей, работающих с API. Надеюсь, вы также найдете их полезными. Я сфокусировался на стандартизации, но максимально сохраняя их простоту и адаптируемость.
Для демонстрации, я начну с типичного теста и проведу его рефакторинг шаг за шагом. Наш первый тест проверяет, может ли пользователь получить свой собственный список категорий:
public function test_users_can_get_their_categories_list()
{
factory(Category::class, 5)->create();
$user = factory(User::class)->create();
Passport::actingAs($user);
$response = $this->get('api/categories');
$response->assertSuccessful();
$response->assertJsonCount(5);
$response->assertJsonStructure([
[
'id',
'name',
'type',
],
]);
foreach ($response->json() as $category) {
$this->assertEquals($user->id,$category['user_id']);
}
}
Очень простой и понятный тест, но тут я спросил себя, зачем мне возвращать $response, если я знаю, что это цепочный класс? Я понял, что обычно делаю это, так как у меня есть кастомные ассерты (команды assert), например кнопка с тремя строками, или иногда мне нужно продебажить ответ и посмотреть JSON.
public function test_users_can_get_their_categories_list()
{
// ...
$response = $this->get('api/categories')
->assertSuccessful()
->dump()
->assertJsonCount(5)
->assertJsonStructure([
[
'id',
'name',
'type',
],
]);
foreach ($response->json() as $category) {
$this->assertEquals($user->id,$category['user_id']);
}
}
TestResponse поставляется с хелпером dump, которая позволяет вам дебажить JSON в цепочном формате, единственное, что нас может беспокоить — это кастомные ассерты.
И снова тут нам поможет TestResponse, потому что для него можно делать макросы. Поэтому я продолжаю работать с цепочками, подставляя новый макрос.
// добавьте этот макрос в ваш сервис-провайдер
TestResponse::macro('assertJsonPaths', function ($path, $expected) {
foreach ($this->json($path) as $real) {
PHPUnit::assertEquals($expected, $real);
}
return $this;
});
// рефакторим тест
public function test_users_can_get_their_categories_list()
{
factory(Category::class, 5)->create();
$user = factory(User::class)->create();
Passport::actingAs($user);
$this->get('api/categories')
->assertSuccessful()
->assertJsonCount(5)
->assertJsonPaths('*.user_id', $user->id)
->assertJsonStructure([
[
'id',
'name',
'type',
],
]);
}
В этом примере работа с Passport выглядит круто, потому что он находится прямо над get-запросом, но это не всегда так, и, к сожалению, хелпер actingAs в TestCase не поддерживает все функции Passport. Поэтому я решил добавить новый метод в TestCase и заменить им любое использование Passport::acting, действующий как часть цепочного формата.
// добавьте этот метод в ваш TestCase
public function passportAs($user, $scopes = [], $guard = 'api')
{
Passport::actingAs($user, $scopes, $guard);
return $this;
}
// снова рефакторим этот тест
public function test_users_can_get_their_categories_list()
{
factory(Category::class, 5)->create();
$user = factory(User::class)->create();
$this->passportAs($user)
// если вы не используйте passport-скоуп , то можете тогда сделать так
// ->actingAs($user,'api')
->get('api/categories')
->assertSuccessful()
->assertJsonCount(5)
->assertJsonPaths('*.user_id', $user->id)
->assertJsonStructure([
[
'id',
'name',
'type',
],
]);
}
Для меня это более читабельный формат. И он не только минимизирует количество строк и промежуточных переменных, но и предотвращает смешивание ассертов ответа с другими ассертами, если таковые имеются. Но погодите-ка, наша фаза подготовки все же выглядит неправильно.
Мы ожидаем, что Категории будут связаны с Пользователем, но Пользователь создается после них. Первое, о чем подумает любой разработчик, — тест неправильный и его необходимо реорганизовать, но если он попытается запустить тест, то тот выполнится успешно!
Если мы хотим писать тесты для людей, в том числе, в качестве проектной документации, а не только для CI/CD (Continuous integration / Continuous delivery — «Непрерывная интеграция / Непрерывное развертывание»), то нам нужно избавляться от любых магических эффектов, четко упоминая их в тесте. Большинство из этих сценариев будут в фазе подготовки и из-за событийного-ориентированного подхода к разработке. Итак, как мы можем это решить?
// Вариант 1, написать простой комментарий
public function test_users_can_get_their_categories_list()
{
factory(Category::class, 5)->create();
// как только пользователь будет создан, у нас будет копия дефолтных категорий
$user = factory(User::class)->create();
// ...
}
// Вариант 2, используйте предоставляемую PHPUnit функцию зависимостей
/**
* @depends test_user_have_a_copy_of_the_default_categories
*/
public function test_users_can_get_their_categories_list()
{
factory(Category::class, 5)->create();
$user = factory(User::class)->create();
// ...
}
Первый вариант самый простой и может использоваться в любом случае, но он не даст нам solid-зависимости между самими тестовыми примерами. Второй вариант является абсолютным победителем, поскольку он позволяет разработчикам и компьютерам знать, какие тесты зависят друг от друга, но сейчас он поддерживает только одноклассовую зависимость — черновой pull-request для поддержки многоклассовых зависимостей пока в процессе — но вы пока можете использовать смесь их обоих прямо сейчас.
Финальная версия теста:
/**
* @depends test_user_have_a_copy_of_the_default_categories
*/
public function test_users_can_get_their_categories_list()
{
factory(Category::class, 5)->create();
$this->passportAs($user = factory(User::class)->create())
->get('api/categories')
->assertSuccessful()
->assertJsonCount(5)
->assertJsonPaths('*.user_id', $user->id)
->assertJsonStructure([
[
'id',
'name',
'type',
],
]);
}
В следующей статье я расскажу вам еще 5 хитростей, которые помогут сделать ваши наборы тестов более стандартизированным, читабельным, обслуживаемым и быстрым.
Автор: Mohammed S. Shurrab
Перевод: Алексей Широков
Наш Телеграм-канал — следите за новостями о Laravel.
