Простота — один из главных факторов, почему люди выбирает 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.