Testes no Laravel com Pest
1. Introdução ao Pest PHP
Pest é um framework de testes moderno construído sobre o PHPUnit, projetado para tornar a escrita de testes mais expressiva e agradável. No ecossistema Laravel, o Pest se destaca por sua sintaxe limpa e funções auxiliares que simplificam tarefas comuns de teste.
Diferenças entre Pest e PHPUnit tradicional:
- Pest usa closures em vez de classes de teste
- Sintaxe mais concisa com funções
test(),it()edescribe() - Encadeamento de expectativas com
expect()etoBe() - Menos boilerplate e configuração manual
Instalação no Laravel:
composer require pestphp/pest --dev
composer require pestphp/pest-plugin-laravel --dev
php artisan pest:install
Após a instalação, o arquivo tests/Pest.php será criado automaticamente com configurações básicas.
2. Estrutura de Testes com Pest
A organização segue a mesma estrutura do Laravel: tests/Unit para testes unitários e tests/Feature para testes de funcionalidade.
Funções helpers principais:
// test() - função principal
test('soma dois números', function () {
$resultado = 2 + 2;
expect($resultado)->toBe(4);
});
// it() - estilo mais descritivo
it('pode criar um usuário', function () {
$user = User::factory()->create();
expect($user)->toBeInstanceOf(User::class);
});
// describe() - agrupa testes relacionados
describe('Operações matemáticas', function () {
test('adição', function () {
expect(1 + 1)->toBe(2);
});
test('subtração', function () {
expect(5 - 3)->toBe(2);
});
});
Arquivo Pest.php:
uses(Tests\TestCase::class)->in('Feature');
uses(Tests\TestCase::class)->in('Unit');
3. Testes de Unidade (Unit Tests)
Testes unitários focam em classes e métodos isolados, sem dependências externas.
Exemplo de teste unitário:
test('calcula preço com desconto', function () {
$produto = new Produto();
$produto->preco = 100;
$precoFinal = $produto->aplicarDesconto(10);
expect($precoFinal)->toBe(90.0);
});
Uso de mocks e spies:
test('envia email de boas-vindas', function () {
$mailer = Mockery::mock(Mailer::class);
$mailer->shouldReceive('send')
->once()
->with(Mockery::type(WelcomeEmail::class));
$service = new UserService($mailer);
$service->registerUser(['name' => 'João']);
});
Expectativas encadeadas:
test('valida dados do usuário', function () {
$validator = new UserValidator();
$resultado = $validator->validate([
'name' => 'Maria',
'email' => 'maria@example.com'
]);
expect($resultado)
->toBeTrue()
->and($validator->errors())
->toBeEmpty();
});
4. Testes de Funcionalidade (Feature Tests)
Testes de funcionalidade simulam requisições HTTP e verificam respostas completas.
Testando rotas e controllers:
test('lista todos os produtos', function () {
Produto::factory()->count(3)->create();
$response = $this->getJson('/api/produtos');
$response->assertStatus(200)
->assertJsonCount(3)
->assertJsonStructure([
'*' => ['id', 'nome', 'preco']
]);
});
test('cria novo produto com sucesso', function () {
$dados = [
'nome' => 'Notebook',
'preco' => 3500.00
];
$response = $this->postJson('/api/produtos', $dados);
$response->assertStatus(201)
->assertJson([
'nome' => 'Notebook',
'preco' => 3500.00
]);
$this->assertDatabaseHas('produtos', $dados);
});
Simulação de diferentes métodos HTTP:
test('atualiza produto', function () {
$produto = Produto::factory()->create();
$response = $this->putJson("/api/produtos/{$produto->id}", [
'preco' => 4000.00
]);
$response->assertOk();
expect($produto->fresh()->preco)->toBe(4000.00);
});
test('deleta produto', function () {
$produto = Produto::factory()->create();
$response = $this->deleteJson("/api/produtos/{$produto->id}");
$response->assertNoContent();
$this->assertDatabaseMissing('produtos', ['id' => $produto->id]);
});
5. Testes com Banco de Dados
Configuração com RefreshDatabase:
<?php
uses(Tests\TestCase::class)
->in('Feature')
->group('database');
Uso de factories e seeders:
test('relacionamento entre usuário e pedidos', function () {
$user = User::factory()
->has(Pedido::factory()->count(3))
->create();
expect($user->pedidos)->toHaveCount(3);
expect($user->pedidos->first()->user_id)->toBe($user->id);
});
test('consulta com escopo global', function () {
Produto::factory()->create(['ativo' => true]);
Produto::factory()->create(['ativo' => false]);
$ativos = Produto::ativos()->get();
expect($ativos)->toHaveCount(1);
});
Testando relacionamentos Eloquent:
test('cria pedido com itens', function () {
$pedido = Pedido::factory()
->has(ItemPedido::factory()->count(2), 'itens')
->create();
expect($pedido->itens)
->toHaveCount(2)
->each->toBeInstanceOf(ItemPedido::class);
$this->assertDatabaseCount('itens_pedido', 2);
});
6. Testes de Autenticação e Autorização
Simulação de usuários autenticados:
test('usuário autenticado acessa dashboard', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)
->get('/dashboard');
$response->assertOk();
});
test('usuário não autenticado é redirecionado', function () {
$response = $this->get('/dashboard');
$response->assertRedirect('/login');
});
Testando gates e policies:
test('apenas admin pode deletar usuários', function () {
$admin = User::factory()->create(['role' => 'admin']);
$user = User::factory()->create();
$response = $this->actingAs($admin)
->delete("/api/users/{$user->id}");
$response->assertOk();
$this->actingAs(User::factory()->create(['role' => 'user']))
->delete("/api/users/{$user->id}")
->assertForbidden();
});
Testando middlewares:
test('middleware verifica assinatura ativa', function () {
$user = User::factory()->create();
$user->subscription()->create(['active_until' => now()->subDay()]);
$response = $this->actingAs($user)
->get('/premium-content');
$response->assertRedirect('/subscription/expired');
});
7. Testes de Filas, Jobs e Notificações
Fake de filas com Queue::fake():
test('job é enviado para fila após registro', function () {
Queue::fake();
$this->post('/register', [
'name' => 'João',
'email' => 'joao@example.com',
'password' => 'password123'
]);
Queue::assertPushed(SendWelcomeEmail::class);
Queue::assertPushed(function (SendWelcomeEmail $job) {
return $job->user->email === 'joao@example.com';
});
});
Testando eventos e listeners:
test('evento é disparado ao criar pedido', function () {
Event::fake();
$pedido = Pedido::factory()->create();
Event::assertDispatched(PedidoCriado::class);
Event::assertDispatched(function (PedidoCriado $event) use ($pedido) {
return $event->pedido->id === $pedido->id;
});
});
Testando notificações:
test('notificação é enviada após compra', function () {
Notification::fake();
$user = User::factory()->create();
$this->actingAs($user)->post('/comprar', ['produto_id' => 1]);
Notification::assertSentTo(
$user,
CompraRealizadaNotification::class
);
Notification::assertSentTo(
$user,
function (CompraRealizadaNotification $notification) {
return $notification->valor > 0;
}
);
});
8. Boas Práticas e Otimização
Organização com describe() e grupos:
describe('API de Produtos', function () {
beforeEach(function () {
$this->user = User::factory()->create();
});
describe('GET /api/produtos', function () {
test('retorna lista paginada', function () {
Produto::factory()->count(15)->create();
$response = $this->actingAs($this->user)
->getJson('/api/produtos?per_page=10');
$response->assertJsonCount(10, 'data');
});
})->group('api', 'produtos');
});
Uso de datasets para testes parametrizados:
test('valida campos obrigatórios', function ($campo, $valor) {
$response = $this->postJson('/api/produtos', [
$campo => $valor
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors($campo);
})->with([
['nome', ''],
['preco', null],
['preco', -10],
['categoria_id', 'invalido']
]);
Cobertura de código e integração com CI/CD:
# Gerar relatório de cobertura
./vendor/bin/pest --coverage
# Executar apenas testes de um grupo
./vendor/bin/pest --group=api
# Integração com GitHub Actions
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
pest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- run: composer install
- run: php artisan key:generate
- run: ./vendor/bin/pest
Referências
- Documentação Oficial do Pest PHP — Guia completo de instalação, configuração e uso do Pest para testes em PHP.
- Pest Plugin Laravel — Plugin oficial que integra Pest com Laravel, fornecendo helpers específicos.
- Laravel Testing Documentation — Documentação oficial do Laravel sobre testes, incluindo HTTP tests, database testing e mocks.
- Pest Testing Laravel: The Definitive Guide — Artigo do Laravel News com exemplos práticos de testes usando Pest no Laravel.
- Mockery Documentation — Documentação oficial do Mockery, framework de mocks amplamente usado com Pest e PHPUnit.
- Pest: A New Way to Test Laravel Applications — Tutorial em vídeo do Laracasts sobre introdução ao Pest no Laravel.
- Testing Laravel with Pest - Codecourse — Curso completo de testes no Laravel usando Pest, abordando desde conceitos básicos até avançados.