Testes para Back-end com Slim Framework: unitários, integração e HTTP

Capítulo 14

Tempo estimado de leitura: 11 minutos

+ Exercício

Por que testar em camadas (unitário, integração e HTTP)

Em um back-end com Slim, é útil separar testes por objetivo: unitários validam regras de domínio e casos de uso isolados (rápidos e determinísticos), integração valida a conversa com infraestrutura real (ex.: repositório + banco), e HTTP/rotas valida o comportamento do sistema “de fora”, exercitando handlers, middlewares e serialização de respostas. Essa separação reduz falsos positivos/negativos e facilita localizar a origem de falhas.

TipoO que validaDependênciasQuando usar
UnitárioRegras de domínio, casos de uso, mapeamentosDoubles (mocks/stubs/fakes)Para garantir lógica e contratos
IntegraçãoRepositórios, queries, transações, constraintsBanco real (ou container), fixturesPara garantir persistência correta
HTTPRotas, middlewares, headers, JSON, statusApp Slim + request/response simuladosPara garantir API conforme contrato

Ferramentas e organização de pastas

Uma estrutura comum de testes (ajuste ao seu projeto):

tests/ Unit/ Domain/ Application/ Integration/ Infrastructure/Repository/ Http/ Routes/ Middleware/ ErrorHandling/ Support/ factories/ fixtures/ bootstrap.php

Recomendações práticas:

  • Use phpunit como runner.
  • Crie uma base de testes para compartilhar helpers (ex.: criar app, criar request, decodificar JSON).
  • Separe Unit, Integration e Http para evitar misturar dependências e tempos de execução.

Exemplo de phpunit.xml (com suites)

<?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="tests/bootstrap.php" colors="true">   <testsuites>     <testsuite name="unit">       <directory>tests/Unit</directory>     </testsuite>     <testsuite name="integration">       <directory>tests/Integration</directory>     </testsuite>     <testsuite name="http">       <directory>tests/Http</directory>     </testsuite>   </testsuites>   <php>     <env name="APP_ENV" value="test"/>   </php> </phpunit>

Testes unitários: domínio e casos de uso com doubles

Conceito: testar comportamento, não implementação

Em testes unitários, o foco é: dado um input, o caso de uso produz um output (ou exceção) esperado e interage com suas dependências por meio de interfaces. Para isso, substituímos repositórios/serviços por doubles:

  • Stub: retorna valores pré-definidos.
  • Mock: além de retornar, verifica chamadas (quantidade/argumentos).
  • Fake: implementação simples em memória (útil quando mocks ficam complexos).

Exemplo de caso de uso e contratos

<?php  interface UserRepository {     public function findByEmail(string $email): ?User;     public function save(User $user): void; }  interface PasswordHasher {     public function hash(string $plain): string; }  final class RegisterUserInput {     public function __construct(         public readonly string $email,         public readonly string $password     ) {} }  final class RegisterUserOutput {     public function __construct(         public readonly string $id,         public readonly string $email     ) {} }  final class EmailAlreadyExists extends DomainException {}  final class RegisterUser {     public function __construct(         private UserRepository $users,         private PasswordHasher $hasher     ) {}      public function execute(RegisterUserInput $in): RegisterUserOutput {         $existing = $this->users->findByEmail($in->email);         if ($existing) {             throw new EmailAlreadyExists();         }         $hash = $this->hasher->hash($in->password);         $user = User::register($in->email, $hash);         $this->users->save($user);         return new RegisterUserOutput($user->id(), $user->email());     } }

Passo a passo: teste unitário com mocks (PHPUnit)

Objetivo: garantir que (1) e-mail duplicado lança exceção, (2) fluxo feliz salva usuário e retorna output.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

<?php  use PHPUnit\Framework\TestCase;  final class RegisterUserTest extends TestCase {     public function test_it_throws_when_email_already_exists(): void {         $users = $this->createMock(UserRepository::class);         $hasher = $this->createMock(PasswordHasher::class);          $users->method('findByEmail')->with('a@a.com')->willReturn(User::fromExisting('1', 'a@a.com', 'hash'));          $useCase = new RegisterUser($users, $hasher);          $this->expectException(EmailAlreadyExists::class);         $useCase->execute(new RegisterUserInput('a@a.com', 'secret'));     }      public function test_it_saves_user_and_returns_output(): void {         $users = $this->createMock(UserRepository::class);         $hasher = $this->createMock(PasswordHasher::class);          $users->method('findByEmail')->with('new@a.com')->willReturn(null);         $hasher->method('hash')->with('secret')->willReturn('hashed');          $users->expects($this->once())->method('save')->with($this->callback(function(User $u) {             return $u->email() === 'new@a.com';         }));          $useCase = new RegisterUser($users, $hasher);         $out = $useCase->execute(new RegisterUserInput('new@a.com', 'secret'));          $this->assertSame('new@a.com', $out->email);         $this->assertNotEmpty($out->id);     } }

Quando preferir fakes em memória

Se o caso de uso interage com o repositório de forma mais rica (ex.: múltiplas consultas, paginação, filtros), mocks podem ficar frágeis. Um fake em memória reduz acoplamento ao “como” e foca no “o quê”.

<?php  final class InMemoryUserRepository implements UserRepository {     private array $items = [];      public function findByEmail(string $email): ?User {         foreach ($this->items as $u) {             if ($u->email() === $email) return $u;         }         return null;     }      public function save(User $user): void {         $this->items[$user->id()] = $user;     } }

Testes de integração: repositórios com banco, transações e fixtures

Conceito: validar SQL, mapeamento e constraints

Teste de integração deve usar o repositório real e um banco real (idealmente isolado para testes). O objetivo é pegar problemas que unitários não pegam: joins errados, colunas, tipos, constraints, transações, índices, etc.

Estratégia 1: transação por teste (rollback no tearDown)

Essa estratégia é rápida e mantém o banco limpo, desde que o seu código use a mesma conexão (ou você consiga garantir isso no teste).

<?php  use PHPUnit\Framework\TestCase;  final class UserRepositoryPdoIntegrationTest extends TestCase {     private PDO $pdo;     private UserRepository $repo;      protected function setUp(): void {         $this->pdo = TestDatabase::pdo(); // helper que cria PDO para o banco de teste         $this->pdo->beginTransaction();         $this->repo = new PdoUserRepository($this->pdo);     }      protected function tearDown(): void {         if ($this->pdo->inTransaction()) {             $this->pdo->rollBack();         }     }      public function test_save_and_find_by_email(): void {         $user = User::register('int@a.com', 'hashed');         $this->repo->save($user);          $found = $this->repo->findByEmail('int@a.com');         $this->assertNotNull($found);         $this->assertSame('int@a.com', $found->email());     } }

Estratégia 2: fixtures controladas (seed mínimo por cenário)

Fixtures devem ser pequenas e específicas. Evite “dump” de banco inteiro. Uma abordagem simples é ter scripts SQL por cenário e executá-los no setUp.

<?php  final class Fixtures {     public static function load(PDO $pdo, string $name): void {         $sql = file_get_contents(__DIR__ . '/fixtures/' . $name . '.sql');         $pdo->exec($sql);     } }
<?php  public function test_find_returns_null_when_not_exists(): void {     Fixtures::load($this->pdo, 'users_empty');     $found = $this->repo->findByEmail('missing@a.com');     $this->assertNull($found); }

Boas práticas para integração com banco

  • Garanta que o banco de teste é isolado (schema próprio) e que migrations já foram aplicadas.
  • Evite dependência entre testes: cada teste prepara seu estado (via transação/fixtures).
  • Teste também falhas esperadas: constraint de e-mail único, foreign keys, etc.
  • Se seu repositório usa transações internas, valide efeitos atômicos (ex.: duas tabelas).

Testes HTTP: simulando Request/Response e validando contrato

Conceito: testar a API como consumidor

Testes HTTP devem validar: status code, headers (ex.: Content-Type), e payload JSON (estrutura e valores). Eles também ajudam a garantir que middlewares e error handlers estão conectados corretamente.

Passo a passo: helper para criar App e executar request

Crie um helper que constrói o app em modo teste e permite executar uma requisição. A ideia é: montar um ServerRequestInterface, chamar $app->handle($request) e inspecionar o ResponseInterface.

<?php  use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\App; use Slim\Psr7\Factory\ServerRequestFactory;  final class HttpTestHelper {     public static function app(): App {         return AppFactoryForTests::create(); // factory do app com container/config de teste     }      public static function request(string $method, string $path, array $jsonBody = [], array $headers = []): ServerRequestInterface {         $factory = new ServerRequestFactory();         $req = $factory->createServerRequest($method, $path);         foreach ($headers as $k => $v) {             $req = $req->withHeader($k, $v);         }         if (!empty($jsonBody)) {             $req->getBody()->write(json_encode($jsonBody));             $req = $req->withHeader('Content-Type', 'application/json');         }         return $req;     }      public static function json(ResponseInterface $res): array {         $res->getBody()->rewind();         return json_decode((string)$res->getBody(), true) ?? [];     } }

Teste de rota: status, headers e JSON

<?php  use PHPUnit\Framework\TestCase;  final class RegisterUserRouteTest extends TestCase {     public function test_register_user_returns_201_and_json(): void {         $app = HttpTestHelper::app();          $req = HttpTestHelper::request('POST', '/users', [             'email' => 'http@a.com',             'password' => 'secret'         ]);          $res = $app->handle($req);         $this->assertSame(201, $res->getStatusCode());         $this->assertStringContainsString('application/json', $res->getHeaderLine('Content-Type'));          $json = HttpTestHelper::json($res);         $this->assertArrayHasKey('data', $json);         $this->assertSame('http@a.com', $json['data']['email']);         $this->assertNotEmpty($json['data']['id']);     } }

Validando erros HTTP padronizados

Além do “happy path”, valide que erros retornam o formato combinado (por exemplo: {"error":{"code","message","details"}}) e o status correto.

<?php  final class RegisterUserErrorsRouteTest extends TestCase {     public function test_duplicate_email_returns_409_with_standard_error(): void {         $app = HttpTestHelper::app();          // 1) cria usuário         $req1 = HttpTestHelper::request('POST', '/users', [             'email' => 'dup@a.com',             'password' => 'secret'         ]);         $app->handle($req1);          // 2) tenta criar novamente         $req2 = HttpTestHelper::request('POST', '/users', [             'email' => 'dup@a.com',             'password' => 'secret'         ]);         $res2 = $app->handle($req2);          $this->assertSame(409, $res2->getStatusCode());         $json = HttpTestHelper::json($res2);         $this->assertArrayHasKey('error', $json);         $this->assertSame('EMAIL_ALREADY_EXISTS', $json['error']['code']);         $this->assertNotEmpty($json['error']['message']);     } }

Testando middlewares: unidade e integração no pipeline

Conceito: middleware como função de transformação/guarda

Um middleware pode: bloquear (retornar resposta sem chamar o próximo), enriquecer request (atributos), ou enriquecer response (headers). Testes devem cobrir esses três comportamentos.

Passo a passo: teste unitário de middleware com handler “fake”

Você pode testar um middleware isoladamente criando um RequestHandlerInterface fake que retorna uma resposta conhecida.

<?php  use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Slim\Psr7\Response;  final class FakeHandler implements RequestHandlerInterface {     public function handle(ServerRequestInterface $request): Response {         $res = new Response(200);         $res->getBody()->write(json_encode(['ok' => true]));         return $res->withHeader('Content-Type', 'application/json');     } }  final class CorrelationIdMiddlewareTest extends TestCase {     public function test_it_adds_correlation_id_header(): void {         $mw = new CorrelationIdMiddleware();         $req = HttpTestHelper::request('GET', '/ping');          $res = $mw->process($req, new FakeHandler());          $this->assertSame(200, $res->getStatusCode());         $this->assertNotEmpty($res->getHeaderLine('X-Correlation-Id'));     } }

Teste de integração do middleware no app (ordem importa)

Alguns comportamentos dependem da ordem no pipeline (ex.: autenticação antes de autorização). Nesse caso, valide via teste HTTP que o middleware está realmente aplicado.

<?php  final class AuthMiddlewareHttpTest extends TestCase {     public function test_protected_route_requires_token(): void {         $app = HttpTestHelper::app();         $req = HttpTestHelper::request('GET', '/me');         $res = $app->handle($req);          $this->assertSame(401, $res->getStatusCode());         $json = HttpTestHelper::json($res);         $this->assertSame('UNAUTHORIZED', $json['error']['code']);     } }

Testando error handlers: exceções, mapeamento e consistência

Conceito: garantir que toda falha vira resposta previsível

Mesmo com exceções diferentes (domínio, validação, infraestrutura), o consumidor da API deve receber um formato consistente. Testes devem cobrir:

  • Exceções de domínio mapeadas para status adequados (ex.: 409, 404).
  • Erros de validação (ex.: 422) com details por campo.
  • Erros inesperados (500) sem vazar detalhes sensíveis.

Passo a passo: teste HTTP para exceção não tratada virar 500 padronizado

Crie uma rota “de teste” apenas no ambiente de teste (ou injete um handler fake) que lança uma exceção e valide o formato.

<?php  final class ErrorHandlerHttpTest extends TestCase {     public function test_unhandled_exception_returns_standard_500(): void {         $app = HttpTestHelper::app();          $req = HttpTestHelper::request('GET', '/__test__/explode');         $res = $app->handle($req);          $this->assertSame(500, $res->getStatusCode());         $json = HttpTestHelper::json($res);         $this->assertSame('INTERNAL_ERROR', $json['error']['code']);         $this->assertArrayNotHasKey('trace', $json['error']);     } }

Checklist de padronização de erros (para transformar em asserts)

  • Content-Type sempre JSON em erro.
  • Campo error.code estável (útil para clientes).
  • error.message legível e sem dados sensíveis.
  • error.details presente apenas quando fizer sentido (ex.: validação).
  • Em 500, não retornar stack trace em ambiente de teste/produção (a menos que seu contrato permita explicitamente).

Estratégia prática de cobertura: o que testar em cada nível

Domínio

  • Entidades/Value Objects: invariantes, normalização, comparações.
  • Exceções de domínio: quando devem ocorrer.

Casos de uso

  • Fluxo feliz e fluxos alternativos.
  • Interações com interfaces (repositórios/serviços) usando mocks ou fakes.
  • Mapeamento de input/output (DTOs) sem depender de HTTP.

Integração

  • Repositórios: salvar, buscar, atualizar, paginação, filtros.
  • Transações: atomicidade e rollback em falhas.
  • Constraints: unicidade, FKs, not null.

HTTP

  • Contrato: status, headers, JSON shape.
  • Middlewares: autenticação/autorização, headers, rate limit (se houver).
  • Error handler: mapeamento de exceções e consistência do payload.

Agora responda o exercício sobre o conteúdo:

Ao organizar testes em um back-end com Slim, qual abordagem melhor reduz falsos positivos/negativos e ajuda a localizar a origem das falhas?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A separação em camadas permite que cada teste valide um aspecto específico (lógica, infraestrutura ou contrato HTTP), evitando dependências desnecessárias e facilitando identificar onde o problema ocorreu.

Próximo capitúlo

Boas práticas de manutenção em Slim Framework: logs, observabilidade e evolução do projeto

Arrow Right Icon
Capa do Ebook gratuito Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa
88%

Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.