{{TOC}}
{{DOCVER 5.3=c06d6a2352ed8c767633aab9c20f2bf7d880c967 28.01.2017 5:00:51, 5.2=6b0b057ae6de3c88cb29188459e38383c622ec23 8.12.2016 23:00:15, 5.1=cdc24ba7426c5b11eb4d050706bd78c3ea4913cc 19.06.2016 20:08:01, 5.0=5d10040a981deee82c0fde0e8e5d2ffc49eaaecb 8.02.2016 18:09:11}}
== Введение ==
Laravel построен с учётом тестирования. Фактически, поддержка ((http://phpunit.de PHPUnit)) доступна по умолчанию, а файл %%(t)phpunit.xml%% уже настроен для вашего приложения. Также фреймворк содержит удобные методы для полноценного тестирования ваших приложений.
Папка %%(t)tests%% содержит файл с примером теста %%(t)ExampleTest.php%%. После установки нового приложения Laravel просто выполните команду %%(sh)phpunit%% для запуска ваших тестов.
=== Среда ===
При выполнении тестов Laravel автоматически задаст настройки среды %%(t)testing%%. Также при тестировании Laravel автоматически настроит сессии и кэш на драйвер %%(t)array%%, а значит данные сессий и кэша не сохранятся при тестировании.
При необходимости вы можете определить другие значения настроек для тестовой среды. Переменные среды %%(t)testing%% можно настроить в файле %%(t)phpunit.xml%%, но перед запуском тестов не забудьте очистить кэш настроек с помощью Artisan-команды %%(sh)config:clear%%!
=== Создание и выполнение тестов ===
Для создания теста используйте Artisan-команду %%(sh)make:test%%:
%%(sh)
php artisan make:test UserTest
%%
Эта команда поместит новый класс %%(t)UserTest%% в папку %%(t)tests%%. Далее вы можете объявлять методы тестов как вы обычно объявляете их для PHPUnit. Для запуска тестов просто выполните команду %%(sh)phpunit%% в терминале:
%%
assertTrue(true);
}
}
%%
.(alert)
Если вы определили собственный метод %%setUp()%%, не забудьте вызвать %%parent::setUp()%%.
.(tl_note)
Начиная с версии Laravel 5.3 последующие разделы данной статьи вынесены в отдельные статьи ((//docs/5.3/application-testing Тестирование приложения)), ((//docs/5.3/database-testing Тестирование БД)) и ((//docs/5.3/mocking Заглушки)). - //прим. пер.//
== Тестирование приложения ==
Laravel предоставляет очень удобный API для создания HTTP-запросов к вашему приложению, проверки вывода, и даже заполнения форм. Например, посмотрим на файл %%(t)ExampleTest.php%% в папке %%(t)tests%%:
%%
visit('/')
->see('Laravel 5')
->dontSee('Rails');
}
}
%%
Метод %%visit()%% делает %%(t)GET%%-запрос в приложение. Метод %%see()%% объявляет, что мы должны увидеть данный текст в отклике приложения. Метод %%dontSee()%% объявляет, что данный текст не возвращается в отклике приложения. Это самый базовый тест приложения в Laravel.
=== Взаимодействие с приложением ===
Само собой, можно делать намного больше, чем просто проверять появление текста в данном отклике. Давайте рассмотрим несколько примеров нажатия ссылок и заполнения форм.
**Нажатие ссылок**
В этом тесте мы сделаем запрос в приложение, "нажмём" ссылку в возвращённом отклике, а затем проверим, что оказались на нужном URI. Например, предположим, что в нашем отклике есть ссылка с текстом "О нас":
%%(html)
О нас
%%
Теперь давайте напишем тест, нажимающий ссылку и проверяющий переход пользователя на правильную страницу:
%%
public function testBasicExample()
{
$this->visit('/')
->click('О нас')
->seePageIs('/about-us');
}
%%
**Работа с формами**
Также Laravel предоставляет несколько методов для тестирования форм. Методы %%type()%%, %%select()%%, %%check()%%, %%attach()%% и %%press()%% позволяют вам взаимодействовать со всеми элементами ввода на ваших формах. Например, представим, что на странице регистрации в приложении есть такая форма:
%%(html)
Метод |
Описание |
$this->type($text, $elementName) | Ввести текст в данное поле |
$this->select($value, $elementName) | Выбрать радио-кнопку или выпадающее поле |
$this->check($elementName) | Поставить чекбокс |
$this->uncheck($elementName) | Снять чекбокс (для 5.2 и выше) |
$this->attach($pathToFile, $elementName) | Прикрепить файл к форме |
$this->press($buttonTextOrElementName) | Нажать кнопку с заданным текстом или именем |
%%
**Работа с вложениями**
Если на вашей форме есть элементы ввода типа %%(t)file%%, вы можете прикрепить файлы к форме методом %%attach()%%:
%%
public function testPhotoCanBeUploaded()
{
$this->visit('/upload')
->type('File Name', 'name')
->attach($absolutePathToFile, 'photo')
->press('Upload')
->see('Upload Successful!');
}
%%
=== Тестирование JSON API ===
Также Laravel предоставляет несколько вспомогательных функций для тестирования JSON API и их откликов. Например, методы %%get()%%, %%post()%%, %%put()%%, %%patch()%% и %%delete()%% используются для выполнения различных HTTP-запросов. Вы также легко можете передать данные и заголовки в эти методы. Для начала давайте напишем тест, выполняющий %%(t)POST%%-запрос к %%(t)/user%% и проверяющий, что данный массив возвращается в формате JSON:
%%
json('POST', '/user', ['name' => 'Sally'])
->seeJson([
'created' => true,
]);
}
}
%%
Метод %%seeJson()%% конвертирует данный массив в JSON, а затем проверяет, что фрагмент JSON появляется **где-либо** внутри полного JSON-отклика, возвращаемого приложением. Поэтому, если в нём будут ещё и другие свойства, этот тест всё равно будет пройден успешно, так как данный фрагмент присутствует в отклике.
**Проверка точного совпадения JSON**
Если вы хотите проверить **точное** совпадение данного массива с возвращённым из приложения JSON, вам надо использовать метод %%seeJsonEquals()%%:
%%
json('POST', '/user', ['name' => 'Sally'])
->seeJsonEquals([
'created' => true,
]);
}
}
%%
%%(DOCNEW 5.2=6b0b057ae6de3c88cb29188459e38383c622ec23 8.12.2016 23:00:15)
**Проверка структуры JSON**
Также можно проверить соответствие структуры JSON определённым требованиям. Для этого служит используйте метод %%seeJsonStructure()%% и передайте в него список (вложенных) ключей:
~%%
get('/user/1')
->seeJsonStructure([
'name',
'pet' => [
'name', 'age'
]
]);
}
}
~%%
В этом примере ожидается получение %%(t)name%% и вложенного объекта %%(t)pet%% с его собственными %%(t)name%% и %%(t)age%%. Метод %%seeJsonStructure()%% выполнится без ошибки, если в отклике будут дополнительные ключи. Например, проверка будет пройдена и в том случае, когда %%(t)pet%% будет иметь атрибут %%(t)weight%%.
Вы можете использовать %%(t)*%%, чтобы задать требование, что возвращаемая структура JSON должна содержать список, в котором каждый элемент содержит по крайней мере те атрибуты, которые есть в наборе значений:
~%%
get('/users')
->seeJsonStructure([
'*' => [
'id', 'name', 'email'
]
]);
}
}
~%%
Вы можете использовать вложение %%(t)*%%. Тогда мы потребуем, что каждый %%(t)user%% в JSON отклике должен содержать заданный набор атрибутов, и каждый %%(t)pet%% каждого %%(t)user%% также должен содержать заданный набор атрибутов:
~%%
$this->get('/users')
->seeJsonStructure([
'*' => [
'id', 'name', 'email', 'pets' => [
'*' => [
'name', 'age'
]
]
]
]);
~%%
%%
=== Сессии / Аутентификация ===
В Laravel есть несколько функций для работы с сессиями во время тестирования. Сначала вы можете задать данные сессии для данного массива при помощи метода %%withSession()%%. Это полезно для загрузки сессии с данными перед выполнением тестового запроса в приложение:
%%
withSession(['foo' => 'bar'])
->visit('/');
}
}
%%
Конечно, чаще всего сессии используют для задания нужного состояния пользователя, например, аутентифицированный пользователь. Простой способ аутентифицировать данного пользователя в качестве текущего - метод %%actingAs()%%. Например, мы можем использовать ((/docs/v5/testing#фабрики-моделей фабрику модели)), чтобы сгенерировать и аутентифицировать пользователя:
%%
create();
$this->actingAs($user)
->withSession(['foo' => 'bar'])
->visit('/')
->see('Hello, '.$user->name);
}
}
%%
%%(DOCNEW 5.2=6b0b057ae6de3c88cb29188459e38383c622ec23 8.12.2016 23:00:15)
Вторым аргументом метода %%actingAs()%% вы можете указать защитника для аутентификации данного пользователя:
~%%
$this->actingAs($user, 'backend')
~%%
%%
=== Отключение посредников ===
При тестировании приложения иногда удобно отключить ((/docs/v5/middleware посредников)) для некоторых тестов. Это позволит вам тестировать маршруты и контроллер изолированно от любых влияний посредников. Laravel содержит простой типаж %%(t)WithoutMiddleware%%, который можно использовать для автоматического отключения всех посредников для класса теста:
%%
withoutMiddleware();
$this->visit('/')
->see('Laravel 5');
}
}
%%
=== Свои HTTP-запросы ===
Если вы хотите сделать свой HTTP-запрос в приложение и получить полный объект %%(t)Illuminate\Http\Response%%, используйте метод %%call()%%:
%%
public function testApplication()
{
$response = $this->call('GET', '/');
$this->assertEquals(200, $response->status());
}
%%
Если вы делаете запросы %%(t)POST%%, %%(t)PUT%% или %%(t)PATCH %%, то можете передать массив входных данных вместе с запросом. Тогда эти данные будут доступны в ваших маршрутах и контроллере через ((/docs/v5/requests экземпляр запроса)):
%%
$response = $this->call('POST', '/user', ['name' => 'Taylor']);
%%
=== Проверки PHPUnit (//assertions//) ===
Laravel предоставляет несколько //assert//-методов для тестов ((https://phpunit.de/ PHPUnit)):
%%(hvlraw)
Метод |
Описание |
->assertResponseOk(); | Проверка того, что клиентский отклик имеет статус ОК |
->assertResponseStatus($code); | Проверка того, что клиентский отклик имеет указанный статус. |
->assertViewHas($key, $value = null); | Проверка того, что представление в отклике содержит данный кусок привязанных данных. |
->assertViewHasAll(array $bindings); | Проверка того, что представление содержит данный список привязанных данных. |
->assertViewMissing($key); | Проверка того, что в представлении в отклике отсутствует данный кусок привязанных данных. |
->assertRedirectedTo($uri, $with = []); | Проверка того, что клиент был переадресован по данному URI |
->assertRedirectedToRoute($name, $parameters = [], $with = []); | Проверка того, что клиент был переадресован по данному маршруту |
->assertRedirectedToAction($name, $parameters = [], $with = []); | Проверка того, что клиент был переадресован к данному действию |
->assertSessionHas($key, $value = null); | Проверка того, что в сессии есть данное значение. |
->assertSessionHasAll(array $bindings); | Проверка того, что в сессии есть данный список значений. |
->assertSessionHasErrors($bindings = [], $format = null); | Проверка того, что в сессии есть привязанные ошибки. |
->assertHasOldInput(); | Проверка того, что в сессии есть введённые ранее данные. |
->assertSessionMissing($key); | Проверка того, что в сессии нет указанного ключа (для версии 5.2 и выше). |
%%
%%(DOCNEW 5.0=5d10040a981deee82c0fde0e8e5d2ffc49eaaecb 8.02.2016 18:09:11)
**Вызов ((docs/v5/controllers контроллера)) из теста**
Вы также можете вызвать из теста любой контроллер:
~%%
$response = $this->action('GET', 'HomeController@index');
$response = $this->action('GET', 'UserController@profile', ['user' => 1]);
~%%
.(alert)
Вам не надо указывать полное пространство имён контроллера при использовании метода %%action()%%. Укажите только ту часть, которая идёт за %%(t)App\Http\Controllers%%.
Метод %%getContent()%% вернёт содержимое-строку ответа. Если ваш маршрут вернёт ((docs/v5/templates %%View%%)), вы можете получить его через свойство %%$original%%:
~%%
$view = $response->original;
$this->assertEquals('John', $view['name']);
~%%
Для вызова HTTPS-маршрута можно использовать метод %%callSecure()%%:
~%%
$response = $this->callSecure('GET', 'foo/bar');
~%%
%%
== Работа с базами данных ==
Также Laravel содержит различные полезные инструменты для упрощения тестирования приложений, использующих БД. Во-первых, вы можете использовать функцию %%seeInDatabase ()%% для проверки наличия в БД данных, подходящих по набору критериев. Например, если мы хотим проверить, что в таблице %%(t)users%% есть запись со значением в поле %%(t)email%% равным %%(t)sally@example.com%%, то можем сделать следующее:
%%
public function testDatabase()
{
// Сделать вызов в приложение...
$this->seeInDatabase('users', ['email' => 'sally@example.com']);
}
%%
Само собой, метод %%seeInDatabase()%% и другие подобные методы служат для удобства. Но вы можете использовать любые встроенные в PHPUnit методы проверок для дополнения своих тестов.
=== Сброс базы данных после каждого теста ===
Часто бывает полезно сбрасывать БД после каждого теста, чтобы данные из предыдущего теста не попадали в последующие тесты.
**Использование миграций**
Один из вариантов - откатывать БД после каждого теста и мигрировать её перед следующим тестом. В Laravel есть простой типаж %%(t)DatabaseMigrations%%, который автоматически сделает это за вас. Просто используйте типаж в классе теста:
%%
visit('/')
->see('Laravel 5');
}
}
%%
**Использование транзакций**
Другой вариант - обернуть каждый тест в транзакцию БД. И снова, в Laravel есть удобный типаж %%(t)DatabaseTransactions%%, который автоматически сделает это за вас:
%%
visit('/')
->see('Laravel 5');
}
}
%%
.(alert)
Этот типаж обернёт в транзакцию только то соединения с БД, которое используется по умолчанию.
=== ((#фабрики-моделей)) Фабрики моделей ===
При тестировании часто необходимо вставить несколько записей в БД перед выполнением теста. Вместо указания значений каждого поля тестовых данных вручную, Laravel позволяет определить набор атрибутов для каждой из ваших ((/docs/v5/eloquent моделей Eloquent)) при помощи "фабрик". Для начала посмотрим на файл %%(t)database/factories/ModelFactory.php%% в вашем приложении. Изначально этот файл содержит одно определение фабрики:
%%
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => bcrypt(str_random(10)),
'remember_token' => str_random(10),
];
});
%%
В замыкание, которое выступает в качестве определения фабрики, вы можете вернуть тестовые значения по умолчанию для всех атрибутов модели. Замыкание получит экземпляр PHP-библиотеки ((https://github.com/fzaninotto/Faker Faker)), которая позволяет вам удобно генерировать случайные данных различных типов для тестирования.
Конечно, вы можете добавить свои собственные дополнительные фабрики в файл %%(t)ModelFactory.php%%. А для улучшения организации вы можете также создать файлы дополнительной фабрики. Например, вы можете создать файлы %%(t)UserFactory.php%% и %%(t)CommentFactory.php%% в папке %%(t)database/factories%%.
**Множественные типы фабрик**
Иногда вам необходимо иметь несколько фабрик для одного класса модели Eloquent. Например, если нужна фабрика для пользователей "Administrator" вдобавок к обычным пользователям. Эти фабрики можно определить методом %%defineAs()%%:
%%
$factory->defineAs(App\User::class, 'admin', function ($faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => str_random(10),
'remember_token' => str_random(10),
'admin' => true,
];
});
%%
Вместо дублирования всех атрибутов из вашей основной фабрики пользователя, вы можете использовать метод %%raw()%% для получения базовых атрибутов. Когда у вас есть атрибуты, просто дополните их любыми необходимыми дополнительными значениями:
%%
$factory->defineAs(App\User::class, 'admin', function ($faker) use ($factory) {
$user = $factory->raw(App\User::class);
return array_merge($user, ['admin' => true]);
});
%%
**Использование фабрик в тестах**
Когда вы определили свои фабрики, вы можете использовать их в своих тестах или файлах для заполнения БД, чтобы генерировать экземпляры модели с помощью глобальной функции %%factory()%%. Давайте рассмотрим несколько примеров создания моделей. Сначала используем метод %%make()%%, который создаёт модели, но не сохраняет их в БД:
%%
public function testDatabase()
{
$user = factory(App\User::class)->make();
// Использование модели в тестах...
}
%%
Если вы хотите переопределить некоторые из значений по умолчанию для своих моделей, вы можете передать массив значений в метод %%make()%%. Будут заменены только указанные значения, а остальные будут иметь значения, определённые в фабрике:
%%
$user = factory(App\User::class)->make([
'name' => 'Abigail',
]);
%%
Также вы можете создать коллекцию моделей или создать модели заданного типа:
%%
// Создать три экземпляра App\User...
$users = factory(App\User::class, 3)->make();
// Создать экземпляр App\User "admin"...
$user = factory(App\User::class, 'admin')->make();
// Создать три экземпляра App\User "admin"...
$users = factory(App\User::class, 'admin', 3)->make();
%%
**Сохранение моделей фабрики**
Метод %%create()%% не только создаёт экземпляры модели, но также сохраняет их в БД при помощи Eloquent-метода %%save()%%:
%%
public function testDatabase()
{
$user = factory(App\User::class)->create();
// Использование модели в тестах...
}
%%
Вы можете переопределить атрибуты для модели, передав массив в метод %%create()%%:
%%
$user = factory(App\User::class)->create([
'name' => 'Abigail',
]);
%%
**Добавление отношений в модели**
Вы можете сохранить в БД даже несколько моделей. В данном примере мы даже прикрепим к созданным моделям отношение. При использовании метода %%create()%% для создания нескольких моделей возвращается ((/docs/v5/eloquent-collections экземпляр коллекции)), позволяя вам использовать любые удобные функции для работы с коллекцией, такие как %%each()%%:
%%
$users = factory(App\User::class, 3)
->create()
->each(function ($u) {
$u->posts()->save(factory(App\Post::class)->make());
});
%%
%%(DOCNEW 5.2=6b0b057ae6de3c88cb29188459e38383c622ec23 8.12.2016 23:00:15)
**Отношения и атрибуты замыкания**
Также вы можете прикрепить к моделям отношения с помощью атрибутов замыканий в определении вашей фабрики. Например, если вы хотите создать экземпляр %%(t)User%% при создании %%(t)Post%%, можно сделать так:
~%%
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => function () {
return factory(App\User::class)->create()->id;
}
];
});
~%%
Эти замыкания также получают подготовленный массив атрибутов фабрики, который содержит их:
~%%
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => function () {
return factory(App\User::class)->create()->id;
},
'user_type' => function (array $post) {
return App\User::find($post['user_id'])->type;
}
];
});
~%%
%%
== Заглушки ==
=== Заглушки событий ===
Если вы используете систему событий Laravel, то при желании можете отключить или заглушить определённые события при тестировании. Например, при тестировании регистрации пользователя вам, вероятно, не нужно вызывать обработчики всех событий %%(t)UserRegistered%%, поскольку они могут посылать письма "добро пожаловать" и т.п.
В Laravel есть удобный метод %%expectsEvents()%%, который проверяет возникновение ожидаемых событий, но предотвращает запуск всех обработчиков для этих событий:
%%
expectsEvents(App\Events\UserRegistered::class);
// Тестирование регистрации пользователя...
}
}
%%
%%(DOCNEW 5.2=6b0b057ae6de3c88cb29188459e38383c622ec23 8.12.2016 23:00:15)
Методом %%doesntExpectEvents()%% можно проверить, что заданные события **не** произошли:
~%%
expectsEvents(App\Events\PodcastWasPurchased::class);
$this->doesntExpectEvents(App\Events\PaymentWasDeclined::class);
// Тестирование покупки подкаста...
}
}
~%%
%%
Если вы хотите предотвратить запуск всех обработчиков событий, используйте метод %%withoutEvents()%%:
%%
withoutEvents();
// Тестирование кода регистрации пользователя...
}
}
%%
=== Заглушки задач ===
Иногда вам может понадобиться просто проверить, что определённые задачи запускаются вашими контроллерами при выполнении запросов в ваше приложение. Это позволяет вам тестировать ваши маршруты / контроллеры изолированно - отдельно от логики вашей задачи. А саму задачу вы можете протестировать в отдельном тест-классе.
В Laravel есть удобный метод %%expectsJobs()%%, который проверяет, что ожидаемые задачи вызываются, но при этом сами задачи выполняться не будут:
%%
expectsJobs(App\Jobs\PurchasePodcast::class);
// Тестирование кода покупки подкаста...
}
}
%%
.(alert)
Этот метод обнаруживает только те задачи, которые запускаются методами типажа %%(t)DispatchesJobs%% или вспомогательной функцией %%dispatch()%%. Он не обнаружит задачу, которая отправлена напрямую в %%Queue::push%%.
=== Заглушки фасадов ===
При тестировании вам может потребоваться отловить вызов к одному из ((docs/v5/facades фасадов)) Laravel. Например, рассмотрим такое действие контроллера:
%%
once()
->with('key')
->andReturn('value');
$this->visit('/users')->see('value');
}
}
%%
.(alert)
Не делайте этого для фасада %%(t)Request%%. Вместо этого, передайте желаемый ввод вспомогательному HTTP-методу, такому как %%call()%% или %%post()%%, во время выполнения вашего теста.
%%(DOCNEW 5.0=5d10040a981deee82c0fde0e8e5d2ffc49eaaecb 8.02.2016 18:09:11)
Рассмотрим такое действие ((docs/v5/controllers контроллера)):
~%%
public function getIndex()
{
Event::fire('foo', ['name' => 'Dayle']);
return 'All done!';
}
~%%
Вы можете отловить вызов класса %%(t)Event%% с помощью метода %%shouldReceive()%% этого фасада, который вернёт экземпляр объекта-заглушки ((https://github.com/padraic/mockery Mockery)).
**Заглушка фасада**
~%%
public function testGetIndex()
{
Event::shouldReceive('fire')->once()->with('foo', ['name' => 'Dayle']);
$this->call('GET', '/');
}
~%%
.(alert)
Не делайте этого для объекта %%(t)Request%%. Вместо этого, передайте желаемый ввод методу %%call()%% во время выполнения вашего теста.
== Вспомогательные методы ==
Класс %%(t)TestCase%% содержит несколько вспомогательных методов для упрощения тестирования вашего приложения.
**Установка и очистка сессий из теста**
~%%
$this->session(['foo' => 'bar']);
$this->flushSession();
~%%
**Установка текущего авторизованного пользователя**
Вы можете установить текущего авторизованного пользователя с помощью метода %%be()%%:
~%%
$user = new User(['name' => 'John']);
$this->be($user);
~%%
**Заполнение БД тестовыми данными**
Вы можете заполнить вашу БД начальными данными из теста методом %%seed()%%:
~%%
$this->seed();
$this->seed('DatabaseSeeder');
~%%
Больше информации на тему начальных данных доступно в ((docs/v5/migrations#начальные разделе о миграциях)).
==Обновление приложения==
Как вы уже возможно знаете, вы можете получить доступ к ((/docs/v5/container сервис-контейнеру)) вашего приложения с помощью %%$this->app%% из любого тестового метода. Этот экземпляр сервис-контейнера обновляется для каждого тестового класса. Если вы хотите вручную обновить приложение для определённого метода, вы можете использовать метод %%refreshApplication()%% из этого тестового метода. Это приведет к сбросу дополнительных привязок, таких как заглушки, которые были помещены в сервис-контейнер после запуска теста.
%%