Імітація (Mocking)
Вступ
Коли тестуєте Laravel-застосунки, ви можете захотіти "імітувати" певні аспекти вашого застосунку, щоб вони фактично не виконувалися під час даного тесту. Наприклад, при тестуванні контролера, який відправляє подію, ви можете захотіти мокати слухачів подій, щоб вони фактично не виконувалися під час тесту. Це дозволяє вам тестувати лише HTTP-відповідь контролера, не турбуючись про виконання слухачів подій, оскільки слухачі подій можуть бути протестовані у власному тестовому випадку.
Laravel надає корисні методи для імітації подій, завдань та інших фасадів з коробки. Ці хелпери в основному забезпечують зручний шар над Mockery, щоб вам не доводилося вручну робити складні виклики методів Mockery.
Імітація об'єктів
Коли ви виконуєте імітацію об'єкта, який буде впроваджено у ваш застосунок через сервіс-контейнер Laravel, вам потрібно буде зв'язати ваш імітований екземпляр у контейнер як прив'язку instance. Це вкаже контейнеру використовувати ваш імітований екземпляр об'єкта замість створення самого об'єкта:
use App\Service;
use Mockery;
use Mockery\MockInterface;
test('something can be mocked', function () {
$this->instance(
Service::class,
Mockery::mock(Service::class, function (MockInterface $mock) {
$mock->expects('process');
})
);
});
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->expects('process');
})
);
}
Щоб зробити це зручнішим, ви можете використовувати метод mock, який надається базовим класом тестового випадку Laravel. Наприклад, наступний приклад еквівалентний наведеному вище:
use App\Service;
use Mockery\MockInterface;
$mock = $this->mock(Service::class, function (MockInterface $mock) {
$mock->expects('process');
});
Ви можете використовувати метод partialMock, коли вам потрібно замокати лише кілька методів об'єкта. Методи, які не замокані, будуть виконуватися нормально при виклику:
use App\Service;
use Mockery\MockInterface;
$mock = $this->partialMock(Service::class, function (MockInterface $mock) {
$mock->expects('process');
});
Аналогічно, якщо ви хочете шпигувати за об'єктом, базовий клас тестового випадку 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, використовуючи метод expects, який поверне екземпляр макету Mockery. Оскільки фасади фактично вирішуються та керуються Laravel Сервіс-контейнером, вони мають набагато більше можливостей для тестування, ніж типові статичні класи. Наприклад, давайте зімітуємо наш виклик до методу get фасаду Cache:
<?php
use Illuminate\Support\Facades\Cache;
test('get index', function () {
Cache::expects('get')
->with('key')
->andReturn('value');
$response = $this->get('/users');
// ...
});
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
class UserControllerTest extends TestCase
{
public function test_get_index(): void
{
Cache::expects('get')
->with('key')
->andReturn('value');
$response = $this->get('/users');
// ...
}
}
Ви не повинні виконувати імітацію фасаду Request. Натомість передайте бажаний вхід у методи HTTP тестування, такі як get і post, під час виконання вашого тесту. Так само, замість імітації фасаду Config, викликайте метод Config::set у ваших тестах.
Шпигуни фасадів
Якщо ви хочете шпигувати за фасадом, ви можете викликати метод spy на відповідному фасаді. Шпигуни схожі на макети; однак, шпигуни записують будь-яку взаємодію між шпигуном і кодом, що тестується, дозволяючи вам робити твердження після виконання коду:
<?php
use Illuminate\Support\Facades\Cache;
test('values are be stored in cache', function () {
Cache::spy();
$response = $this->get('/');
$response->assertStatus(200);
Cache::shouldHaveReceived('put')->with('name', 'Taylor', 10);
});
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')->with('name', 'Taylor', 10);
}
Взаємодія з часом
Коли тестуєте, вам іноді може знадобитися змінити час, що повертається такими хелперами, як now або Illuminate\Support\Carbon::now(). На щастя, базовий клас тестування функцій Laravel включає хелпери, які дозволяють маніпулювати поточним часом:
test('time can be manipulated', function () { // Подорож у майбутнє... $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(); });
public function test_time_can_be_manipulated(): void { // Подорож у майбутнє... $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;
test('forum threads lock after one week of inactivity', function () {
$thread = Thread::factory()->create();
$this->travel(1)->week();
expect($thread->isLockedByInactivity())->toBeTrue();
});
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());
}
