Simulação
Introdução
Ao testar aplicativos Laravel, você pode desejar "simular" certos aspectos do seu aplicativo para que eles não sejam realmente executados durante um determinado teste. Por exemplo, ao testar um controlador que despacha um evento, você pode desejar simular os ouvintes de eventos para que eles não sejam realmente executados durante o teste. Isso permite que você teste apenas a resposta HTTP do controlador sem se preocupar com a execução dos ouvintes de eventos, pois os ouvintes de eventos podem ser testados em seu próprio caso de teste.
O Laravel fornece métodos úteis para simular eventos, trabalhos e outras fachadas prontas para uso. Esses auxiliares fornecem principalmente uma camada de conveniência sobre o Mockery para que você não precise fazer manualmente chamadas complicadas de método do Mockery.
Objetos de simulação
Ao simular um objeto que será injetado em seu aplicativo por meio do service container do Laravel, você precisará vincular sua instância simulada ao contêiner como uma vinculação instance
. Isso instruirá o contêiner a usar sua instância simulada do objeto em vez de construir o próprio objeto:
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->shouldReceive('process')->once();
})
);
});
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();
})
);
}
Para tornar isso mais conveniente, você pode usar o método mock
fornecido pela classe de caso de teste base do Laravel. Por exemplo, o exemplo a seguir é equivalente ao exemplo acima:
use App\Service;
use Mockery\MockInterface;
$mock = $this->mock(Service::class, function (MockInterface $mock) {
$mock->shouldReceive('process')->once();
});
Você pode usar o método partialMock
quando precisar simular apenas alguns métodos de um objeto. Os métodos que não são simulados serão executados normalmente quando chamados:
use App\Service;
use Mockery\MockInterface;
$mock = $this->partialMock(Service::class, function (MockInterface $mock) {
$mock->shouldReceive('process')->once();
});
Da mesma forma, se você quiser espionar um objeto, a classe de caso de teste base do Laravel oferece um método spy
como um wrapper conveniente em torno do método Mockery::spy
. Os espiões são semelhantes aos mocks; no entanto, os espiões registram qualquer interação entre o espião e o código que está sendo testado, permitindo que você faça afirmações após a execução do código:
use App\Service;
$spy = $this->spy(Service::class);
// ...
$spy->shouldHaveReceived('process');
Fachadas de Mocking
Ao contrário das chamadas de métodos estáticos tradicionais, fachadas (incluindo fachadas em tempo real) podem ser simuladas. Isso fornece uma grande vantagem sobre os métodos estáticos tradicionais e garante a mesma testabilidade que você teria se estivesse usando injeção de dependência tradicional. Ao testar, você pode querer simular uma chamada para uma fachada do Laravel que ocorre em um dos seus controladores. Por exemplo, considere a seguinte ação do controlador:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
/**
* Recupere uma lista de todos os usuários do aplicativo.
*/
public function index(): array
{
$value = Cache::get('key');
return [
// ...
];
}
}
Podemos simular a chamada para a fachada Cache
usando o método shouldReceive
, que retornará uma instância de uma simulação Mockery. Como as fachadas são realmente resolvidas e gerenciadas pelo contêiner de serviço do Laravel, elas têm muito mais testabilidade do que uma classe estática típica. Por exemplo, vamos simular nossa chamada para o método get
da fachada Cache
:
<?php
use Illuminate\Support\Facades\Cache;
test('get index', function () {
Cache::shouldReceive('get')
->once()
->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::shouldReceive('get')
->once()
->with('key')
->andReturn('value');
$response = $this->get('/users');
// ...
}
}
AVISO
Você não deve simular a fachada Request
. Em vez disso, passe a entrada que você deseja para os métodos de teste HTTP como get
e post
ao executar seu teste. Da mesma forma, em vez de zombar da fachada Config
, chame o método Config::set
em seus testes.
Facade Spies
Se você quiser espionar uma fachada, você pode chamar o método spy
na fachada correspondente. Os espiões são semelhantes aos mocks; no entanto, os espiões registram qualquer interação entre o espião e o código que está sendo testado, permitindo que você faça afirmações após o código ser executado:
<?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')->once()->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')->once()->with('name', 'Taylor', 10);
}
Interagindo com o tempo
Ao testar, você pode ocasionalmente precisar modificar o tempo retornado por auxiliares como now
ou Illuminate\Support\Carbon::now()
. Felizmente, a classe de teste de recurso base do Laravel inclui auxiliares que permitem que você manipule o tempo atual:
test('time can be manipulated', function () {
// Viaje para o futuro...
$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();
// Viaje ao passado...
$this->travel(-5)->hours();
// Viaje para um tempo específico...
$this->travelTo(now()->subHours(6));
// Retorne ao tempo presente...
$this->travelBack();
});
public function test_time_can_be_manipulated(): void
{
// Viaje para o futuro...
$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();
// Viaje ao passado...
$this->travel(-5)->hours();
// Viaje para um tempo específico...
$this->travelTo(now()->subHours(6));
// Retorne ao tempo presente...
$this->travelBack();
}
Você também pode fornecer um fechamento para os vários métodos de viagem no tempo. O fechamento será invocado com o tempo congelado no tempo especificado. Depois que o fechamento for executado, o tempo será retomado normalmente:
$this->travel(5)->days(function () {
// Teste algo cinco dias no futuro...
});
$this->travelTo(now()->subDays(10), function () {
// Testar algo durante um determinado momento...
});
O método freezeTime
pode ser usado para congelar o tempo atual. Da mesma forma, o método freezeSecond
congelará o tempo atual, mas no início do segundo atual:
use Illuminate\Support\Carbon;
// Congele o tempo e retome o tempo normal após executar o fechamento...
$this->freezeTime(function (Carbon $time) {
// ...
});
// Congela o tempo no segundo atual e retoma o tempo normal após executar o fechamento...
$this->freezeSecond(function (Carbon $time) {
// ...
})
Como seria de se esperar, todos os métodos discutidos acima são principalmente úteis para testar o comportamento do aplicativo sensível ao tempo, como bloquear postagens inativas em um fórum de discussão:
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());
}