Guia para TDD



Este guia define como aplicar TDD no ciclo de desenvolvimento, garantindo que cada funcionalidade seja criada a partir de testes. O objetivo é assegurar qualidade, legibilidade, cobertura de casos críticos e permitir evolução sem medo de regressões.

Nossos projetos adotam o Jest (para Node/TypeScript) e o PHPUnit (para PHP) como frameworks oficiais de testes. Ambos são rápidos, amplamente suportados e oferecem recursos completos como testes unitários, mocks, spies, relatórios de cobertura e suporte a testes assíncronos, garantindo padronização no desenvolvimento.

Todos os testes (unitários, de caso de uso e de integração) devem ser escritos utilizando o framework correspondente à linguagem do módulo, assegurando consistência, facilidade de manutenção e integração com o pipeline de CI/CD.


1. Princípios de TDD

  1. Red → Escreva primeiro um teste que falhe (sem implementação).
  2. Green → Implemente o mínimo de código necessário para o teste passar.
  3. Refactor → Refatore o código mantendo os testes verdes.

2. Pirâmide de Testes

  • Testes Unitários → rápidos, pequenos e isolados (domínio e funções puras).
  • Testes de Caso de Uso / Aplicação → validam regras de negócio e orquestração.
  • Testes de Integração → validam integrações com infraestrutura (banco, APIs externas).

3. Fluxo de Desenvolvimento com TDD

  1. Definir o Requisito → escreva um teste que descreve a regra de negócio esperada.
  2. Teste Unitário (Domínio) → garantir que a regra isolada funciona.
  3. Teste de Caso de Uso → validar orquestração da lógica (com mocks de dependências).
  4. Teste de Integração → validar persistência e contratos externos.
  5. Teste de Aceitação / API (quando necessário) → validar resposta real da aplicação.

4. Frameworks Oficiais

  • Jest → para módulos Node.js/TypeScript.
  • PHPUnit → para módulos PHP.

Ambos são obrigatórios para manter padronização e integração com o pipeline de CI/CD.


5. Exemplos

5.1 Teste Unitário (Domínio)

Jest

// tests/user.spec.ts
import { User } from "../src/domain/User";

describe("User Entity", () => {
  it("deve calcular idade em dias corretamente", () => {
    const user = new User("Sergio", 30);
    expect(user.ageInDays()).toBe(30 * 365);
  });
});

PHPUnit

// tests/UserTest.php
<?php
use PHPUnit\\Framework\\TestCase;
use App\\Domain\\User;

class UserTest extends TestCase
{
    public function testAgeInDays(): void
    {
        $user = new User("Sergio", 30);
        $this->assertEquals(30 * 365, $user->ageInDays());
    }
}


5.2 Teste de Caso de Uso

Jest

// tests/create-user.usecase.spec.ts
import { InMemoryUserRepository } from "../src/infrastructure/InMemoryUserRepository";
import { CreateUserUseCase } from "../src/application/usecases/CreateUserUseCase";

it("deve criar usuário válido", async () => {
  const repo = new InMemoryUserRepository();
  const useCase = new CreateUserUseCase(repo);

  const result = await useCase.execute({ name: "Sergio", age: 30 });

  expect(result).toMatchObject({ name: "Sergio", age: 30 });
  expect(await repo.findAll()).toHaveLength(1);
});

PHPUnit

// tests/CreateUserUseCaseTest.php
<?php
use PHPUnit\\Framework\\TestCase;
use App\\Application\\UseCases\\CreateUserUseCase;
use App\\Infrastructure\\InMemoryUserRepository;

class CreateUserUseCaseTest extends TestCase
{
    public function testCreateValidUser(): void
    {
        $repo = new InMemoryUserRepository();
        $useCase = new CreateUserUseCase($repo);

        $result = $useCase->execute(["name" => "Sergio", "age" => 30]);

        $this->assertEquals("Sergio", $result["name"]);
        $this->assertCount(1, $repo->findAll());
    }
}


5.3 Teste de Integração

Jest

// tests/user-repository.integration.spec.ts
import { Database } from "../src/infrastructure/Database";
import { UserRepository } from "../src/infrastructure/UserRepository";
import { User } from "../src/domain/User";

it("deve persistir e recuperar usuário do banco", async () => {
  const db = new Database(":memory:");
  const repo = new UserRepository(db);

  const user = new User("Sergio", 30);
  await repo.save(user);

  const found = await repo.findById(user.id);
  expect(found).toMatchObject({ name: "Sergio", age: 30 });
});

PHPUnit

// tests/UserRepositoryIntegrationTest.php
<?php
use PHPUnit\\Framework\\TestCase;
use App\\Infrastructure\\Database;
use App\\Infrastructure\\UserRepository;
use App\\Domain\\User;

class UserRepositoryIntegrationTest extends TestCase
{
    public function testPersistAndRetrieveUser(): void
    {
        $db = new Database(":memory:");
        $repo = new UserRepository($db);

        $user = new User("Sergio", 30);
        $repo->save($user);

        $found = $repo->findById($user->getId());
        $this->assertEquals("Sergio", $found->getName());
    }
}


6. Boas Práticas

  • Isolamento: testes unitários nunca devem acessar banco, rede ou APIs externas.
  • Cobertura: priorizar cobertura em regras de negócio e casos críticos.
  • Nomenclatura: descreva a expectativa no nome do teste (deve_criar_usuario_valido).
  • Mocks/Stubs: use apenas quando necessário, não abuse.
  • CI/CD: todos os testes devem rodar automaticamente no pipeline.
    • Caso seja necessário, incluir no github em Secrets and variables as credenciais existentes no projeto, para os testes serem efetuados