#Введение
При тестировании приложений Laravel вы можете захотеть «мокировать» определённые части приложения, чтобы они фактически не выполнялись во время конкретного теста. Например, при тестировании контроллера, который диспатчит событие, вы можете захотеть мокировать слушателей событий, чтобы они не выполнялись во время теста. Это позволяет тестировать только HTTP-ответ контроллера, не беспокоясь о выполнении слушателей событий, так как слушатели можно протестировать отдельно.
Laravel предоставляет удобные методы для мокирования событий, заданий и других фасадов из коробки. Эти помощники в основном служат удобным слоем поверх Mockery, чтобы вам не приходилось вручную вызывать сложные методы Mockery.
#Мокирование объектов
При мокировании объекта, который будет внедрён в ваше приложение через service container Laravel, необходимо привязать мок-экземпляр в контейнер как instance binding. Это укажет контейнеру использовать ваш мок-объект вместо создания объекта самостоятельно:
use App\Service;
use Mockery;
use Mockery\MockInterface;
public function test_something_can_be_mocked(): void
{
$this->instance(
Service::class,
Mockery::mock(Service::class, function (MockInterface $mock) {
$mock->shouldReceive('process')->once();
})
);
}
Для удобства вы можете использовать метод mock, предоставляемый базовым классом теста Laravel. Например, следующий пример эквивалентен приведённому выше:
use App\Service;
use Mockery\MockInterface;
$mock = $this->mock(Service::class, function (MockInterface $mock) {
$mock->shouldReceive('process')->once();
});
Метод partialMock можно использовать, когда нужно мокировать только несколько методов объекта. Методы, которые не мокируются, будут выполняться как обычно при вызове:
use App\Service;
use Mockery\MockInterface;
$mock = $this->partialMock(Service::class, function (MockInterface $mock) {
$mock->shouldReceive('process')->once();
});
Аналогично, если вы хотите шпионить за объектом, базовый класс теста Laravel предлагает метод spy как удобную обёртку над Mockery::spy. Шпионы похожи на моки, но они записывают любое взаимодействие между шпионом и тестируемым кодом, позволяя делать утверждения после выполнения кода:
use App\Service;
$spy = $this->spy(Service::class);
// ...
$spy->shouldHaveReceived('process');
#Мокирование фасадов
В отличие от традиционных вызовов статических методов, фасады (включая фасады реального времени) могут быть мокированы. Это даёт большое преимущество по сравнению с обычными статическими методами и обеспечивает такую же тестируемость, как при использовании традиционного внедрения зависимостей. При тестировании вы часто захотите мокировать вызов фасада Laravel, который происходит в одном из ваших контроллеров. Например, рассмотрим следующий метод контроллера:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
/**
* Получить список всех пользователей приложения.
*/
public function index(): array
{
$value = Cache::get('key');
return [
// ...
];
}
}
Мы можем мокировать вызов фасада Cache, используя метод shouldReceive, который вернёт экземпляр мока из Mockery. Поскольку фасады фактически разрешаются и управляются Laravel service container, они гораздо более тестируемы, чем обычные статические классы. Например, давайте замокируем вызов метода get фасада Cache:
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
class UserControllerTest extends TestCase
{
public function test_get_index(): void
{
Cache::shouldReceive('get')
->once()
->with('key')
->andReturn('value');
$response = $this->get('/users');
// ...
}
}
Не следует мокировать фасад Request. Вместо этого передавайте нужные входные данные в HTTP методы тестирования, такие как get и post при выполнении теста. Аналогично, вместо мокирования фасада Config вызывайте метод Config::set в ваших тестах.
#Шпионы фасадов
Если вы хотите шпионить за фасадом, можно вызвать метод spy на соответствующем фасаде. Шпионы похожи на моки, но записывают любое взаимодействие между шпионом и тестируемым кодом, позволяя делать утверждения после выполнения кода:
use Illuminate\Support\Facades\Cache;
public function test_values_are_be_stored_in_cache(): void
{
Cache::spy();
$response = $this->get('/');
$response->assertStatus(200);
Cache::shouldHaveReceived('put')->once()->with('name', 'Taylor', 10);
}
#Работа со временем
При тестировании иногда нужно изменить время, возвращаемое такими помощниками, как now или Illuminate\Support\Carbon::now(). К счастью, базовый класс функциональных тестов Laravel включает методы, позволяющие манипулировать текущим временем:
use Illuminate\Support\Carbon;
public function test_time_can_be_manipulated(): void
{
// Travel into the future...
$this->travel(5)->milliseconds();
$this->travel(5)->seconds();
$this->travel(5)->minutes();
$this->travel(5)->hours();
$this->travel(5)->days();
$this->travel(5)->weeks();
$this->travel(5)->years();
// Переместиться в прошлое...
$this->travel(-5)->hours();
// Переместиться к конкретному времени...
$this->travelTo(now()->subHours(6));
// Вернуться к текущему времени...
$this->travelBack();
}
Вы также можете передать замыкание в различные методы перемещения во времени. Замыкание будет вызвано с замороженным временем на указанном моменте. После выполнения замыкания время продолжит идти как обычно:
$this->travel(5)->days(function () {
// Тестировать что-то через пять дней в будущем...
});
$this->travelTo(now()->subDays(10), function () {
// Тестировать что-то в заданный момент...
});
Метод freezeTime позволяет заморозить текущее время. Аналогично, метод freezeSecond замораживает время на начало текущей секунды:
use Illuminate\Support\Carbon;
// Заморозить время и вернуть к нормальному после выполнения замыкания...
$this->freezeTime(function (Carbon $time) {
// ...
});
// Заморозить время на текущей секунде и вернуть к нормальному после выполнения замыкания...
$this->freezeSecond(function (Carbon $time) {
// ...
})
Как и ожидалось, все описанные методы в основном полезны для тестирования поведения приложения, чувствительного ко времени, например, блокировки неактивных тем на форуме:
use App\Models\Thread;
public function test_forum_threads_lock_after_one_week_of_inactivity()
{
$thread = Thread::factory()->create();
$this->travel(1)->week();
$this->assertTrue($thread->isLockedByInactivity());
}