Testes de integração e funcionais
Teste como os componentes trabalham juntos na prática.
Testes de integração e funcionais é uma aula grátis de PHP Academy no CoddyKit. Esta é a aula 3 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de PHP Academy, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de PHP Academy inclui 4 aulas no total.
Além dos testes unitários
Os testes unitários comprovam que uma classe funciona isoladamente, com tudo substituído por simulações. Mas as simulações podem enganar — o banco de dados real, o roteador do framework ou a camada HTTP comportam-se de maneira diferente. Os testes de integração verificam dois ou mais componentes reais conectados entre si; os testes funcionais exercitam a aplicação de ponta a ponta por meio do seu ponto de entrada público (uma solicitação HTTP ou um comando de console). Nesta lição, você aprenderá o papel de ambos e como escrevê-los.
A pirâmide de testes
Uma suíte saudável tem o formato de uma pirâmide:
- Muitos testes unitários — rápidos, isolados e executados constantemente.
- Menos testes de integração — usam banco de dados/fila/cache reais e são mais lentos.
- Poucos testes funcionais/de ponta a ponta — usam a pilha completa, são os mais lentos e mais frágeis.
Inverter essa proporção (criando um “cone de sorvete” composto principalmente por testes de ponta a ponta) resulta em uma suíte lenta e instável. Priorize a cobertura nos níveis inferiores sempre que possível.
Integração: repositório contra um banco de dados real
Um teste de integração de um repositório usa uma conexão real com o banco de dados — não uma simulação — para detectar erros de SQL, esquema e mapeamento. Use um banco de dados exclusivo para testes (ou um SQLite em memória cujo dialeto seja suficientemente próximo do seu) e um PDO real.
<?php
use PHPUnit\Framework\TestCase;
final class UserRepositoryTest extends TestCase
{
private \PDO $pdo;
protected function setUp(): void
{
$this->pdo = new \PDO('sqlite::memory:');
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)');
}
public function test_persists_and_finds_a_user(): void
{
$repo = new UserRepository($this->pdo);
$id = $repo->create('ada@example.com');
self::assertSame('ada@example.com', $repo->find($id)->email);
}
}
Isolamento: reversão de transação
Os testes de integração não devem deixar estado residual uns nos outros. O padrão mais rápido é envolver cada teste em uma transação e revertê-la em tearDown() — o banco de dados retorna a um estado limpo sem precisar recriar os dados. (O truncamento é a alternativa quando o código sob teste confirma a transação ou usa DDL.)
<?php
protected function setUp(): void
{
$this->pdo = TestDb::connection();
$this->pdo->beginTransaction();
}
protected function tearDown(): void
{
$this->pdo->rollBack(); // undo everything this test did
}
Dados de teste e fábricas
Os testes precisam de dados. Evite grandes despejos de SQL difíceis de manter; prefira pequenas fábricas que criem apenas as linhas necessárias para um teste, com valores padrão sensatos que você possa substituir em cada caso. Assim, a intenção de cada teste fica explícita e resistente ao crescimento do esquema.
<?php
final class UserFactory
{
public static function create(\PDO $pdo, array $overrides = []): int
{
$data = array_merge(['email' => 'user'.uniqid().'@test.dev'], $overrides);
$stmt = $pdo->prepare('INSERT INTO users (email) VALUES (:email)');
$stmt->execute($data);
return (int) $pdo->lastInsertId();
}
}
Funcional: acessando a aplicação como um cliente
Um teste funcional conduz o núcleo/roteador real da aplicação com uma solicitação simulada e verifica a resposta — código de status, cabeçalhos e corpo. O WebTestCase do Symfony e os auxiliares de testes HTTP do Laravel fazem exatamente isso sem iniciar um servidor web real. A seguir está o formato do Symfony.
<?php
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class HealthControllerTest extends WebTestCase
{
public function test_health_endpoint_returns_ok(): void
{
$client = static::createClient();
$client->request('GET', '/health');
self::assertResponseIsSuccessful(); // 2xx
self::assertJson($client->getResponse()->getContent());
}
}
Verificando a resposta
Os testes funcionais verificam o comportamento observável no limite: status, formato JSON e efeitos colaterais no banco de dados. A seguir, um POST cria um recurso; verificamos tanto a resposta HTTP quanto se o registro realmente foi armazenado — comprovando que toda a pilha cooperou.
<?php
public function test_creating_a_user_persists_it(): void
{
$client = static::createClient();
$client->request('POST', '/users', server: [
'CONTENT_TYPE' => 'application/json',
], content: json_encode(['email' => 'ada@example.com']));
self::assertResponseStatusCodeSame(201);
$repo = static::getContainer()->get(UserRepository::class);
self::assertNotNull($repo->findByEmail('ada@example.com'));
}
Simulando seletivamente as extremidades
Mesmo os testes funcionais não devem chamar provedores de pagamento reais nem enviar e-mails reais. Substitua apenas os serviços externos mais externos — troque-os no contêiner de testes por implementações simuladas/em memória — mantendo real o seu código, o roteamento e o banco de dados. Assim, você preserva a fidelidade de ponta a ponta sem chamadas de rede instáveis.
<?php
public function test_checkout_charges_via_gateway(): void
{
$client = static::createClient();
// Replace the real gateway binding with an in-memory fake:
static::getContainer()->set(PaymentGateway::class, new FakePaymentGateway());
$client->request('POST', '/checkout', content: json_encode(['cart' => 1]));
self::assertResponseIsSuccessful();
}
Suítes separadas, velocidades separadas
Divida o seu phpunit.xml em conjuntos de testes para que a suíte rápida de testes unitários seja executada a cada salvamento, enquanto as suítes lentas de integração/funcionais sejam executadas sob demanda ou na integração contínua. Agrupe por diretório e selecione com --testsuite.
<phpunit>
<testsuites>
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="functional">
<directory>tests/Functional</directory>
</testsuite>
</testsuites>
</phpunit>O determinismo é mais importante que a cobertura
A maneira mais rápida de perder a confiança em uma suíte é permitir instabilidade. Torne determinísticos os testes de integração/funcionais:
- Congele o tempo (injete um relógio) em vez de chamar
time(). - Defina uma semente para a aleatoriedade; evite verificações dependentes da ordem em consultas não ordenadas.
- Redefina o estado do banco de dados a cada teste (reversão de transação/truncamento).
- Nunca dependa da ordem de execução dos testes.
Faça o banco de dados de testes corresponder ao de produção
O SQLite em memória é rápido, mas diferenças de dialeto (sensibilidade a maiúsculas e minúsculas, funções JSON, verificação de chaves estrangeiras e tipos) podem esconder erros que só aparecem no seu mecanismo real. Para código que usa SQL específico do mecanismo, execute os testes de integração no mesmo mecanismo usado em produção — normalmente em um contêiner Docker descartável na integração contínua — em vez de usar um substituto conveniente.
<?php
// Read connection from environment so CI points at a real Postgres/MySQL container
$dsn = getenv('TEST_DATABASE_URL') ?: 'sqlite::memory:';
$pdo = new \PDO($dsn);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// In CI, TEST_DATABASE_URL targets the same engine as production.
Verificação rápida
Como um teste funcional deve tratar um provedor de pagamentos de terceiros?
Recapitulação
Você aprendeu a testar componentes em conjunto:
- Os testes de integração usam bancos de dados/serviços reais para detectar o que as simulações escondem; isole-os com a reversão de transações.
- Os testes funcionais conduzem o núcleo real por meio de solicitações simuladas e verificam as respostas e os efeitos colaterais.
- As fábricas criam dados mínimos; simule apenas os limites externos mais externos.
- Divida as suítes por velocidade e mantenha os testes mais lentos determinísticos.
A seguir: medir a qualidade dos testes com testes de mutação.
Perguntas Frequentes
A aula “Testes de integração e funcionais” é grátis?
Sim — o texto completo de “Testes de integração e funcionais” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de PHP Academy, atualize para CoddyKit PRO. O curso de PHP Academy inclui 4 aulas no total.
O que vou aprender em “Testes de integração e funcionais”?
Teste como os componentes trabalham juntos na prática. Você pratica PHP Academy com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar PHP Academy?
Nenhuma experiência prévia é necessária. PHP Academy no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 3 de 4.
Quanto tempo leva a aula “Testes de integração e funcionais”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de PHP Academy?
Sim. Cada aula de PHP Academy inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- O fluxo de trabalho do desenvolvimento orientado por testes
- Simulação e criação de dublês com Mockery
- Testes de integração e funcionais
- Testes de mutação com Infection