Dominando a Arquitetura de TDD: Guia Prático para Criar Software Testável e Escalável

Dominando a Arquitetura de TDD: Guia Prático para Criar Software Testável e Escalável

“`html





Dominando a Arquitetura de TDD

TDD
Arquitetura que sustenta testes

Dominando a Arquitetura de TDD

Quando eu aplico TDD com disciplina, a arquitetura não “aparece pronta”: ela evolui guiada por exemplos.
O resultado é um sistema com baixo acoplamento, limites claros e testes que protegem comportamento — não implementação.

Nível: técnico • Foco: design orientado a testes


1) Comece pelos limites: o que entra, o que sai, o que é regra

A “arquitetura de TDD” começa definindo fronteiras. Antes de pensar em classes, eu separo:
entrada (comandos/requests), saída (respostas/eventos) e regra (decisões do domínio).
Isso evita o erro mais comum: criar infraestrutura e depois tentar encaixar testes.

Regra prática: se um teste precisa “driblar” dependências externas (banco, HTTP, relógio), eu tenho um limite mal desenhado.
O TDD quer que a regra rode rápido e determinística.
  • Use camadas por intenção: controllers/adapters de um lado, aplicação (casos de uso) no meio, domínio (regras) no outro.
  • Defina contratos (interfaces/DTOs/valores): testes devem validar contratos claros.
  • Trate efeitos colaterais como bordas: persistência, rede e tempo ficam fora do núcleo.


2) Estruture casos de uso por exemplos (e não por entidades)

A cada ciclo do TDD, eu faço uma pergunta: qual comportamento do sistema eu estou descrevendo?
Em vez de organizar por “tabela X” ou “entidade Y”, eu organizo por fluxos.
Isso reduz mudanças futuras quando o modelo do domínio evolui.

  • Caso de uso = uma orquestração pequena e explícita: validação, chamadas ao domínio, persistência e resposta.
  • Domínio = regras puras (quando possível) que podem ser testadas isoladamente.
  • Adapters = implementação dos contratos. Eles trocam sem exigir reescrita do núcleo.
Um bom sinal: eu consigo escrever testes de caso de uso sem precisar “montar o mundo”.
Se eu preciso de muitos mocks por mock, eu provavelmente estou testando implementação e não comportamento.

Técnica que eu uso: “primeiro o contrato do caso de uso”. Eu descrevo o input e output que importam para o consumidor.
Só depois eu conecto a regra interna e as dependências.


3) Controle o acoplamento: dependências como portas, não como detalhes

Arquitetura que escala com TDD depende de portas (interfaces) e adapters (implementações).
O teste deve falar com a porta, não com o detalhe.

Em termos práticos, eu defino contratos para o que o caso de uso precisa:
repositórios, geradores de IDs, relógio, serviço externo, mensageria.
O núcleo não “conhece” tecnologia.

  • Evite chamar diretamente frameworks (ex.: ORM/HTTP) dentro do núcleo.
  • Prefira valores imutáveis (ex.: Money, Email, Status) para reduzir estado surpresa.
  • Injeção de dependência mínima: só o que o caso de uso consome.
  • Falhas explícitas: erros retornam tipos/resultado; não exceções genéricas espalhadas.
Se eu quero testar uma regra, eu mantenho o teste com o mínimo de tecnologia e máximo de intenção.
Se eu quero testar persistência/HTTP, eu monto testes separados para o adapter.


4) Refatore com segurança: o ciclo TDD como motor de design

Refatoração no TDD não é um evento; é um modo de trabalho.
Eu mantenho o foco no comportamento e uso o conjunto de testes como “rede”.
Dessa forma, quando a arquitetura precisa evoluir, eu faço isso com mudança pequena por vez.

  • Red-Green-Refactor: eu escrevo o teste (Red), implemento o mínimo (Green), e refatoro (Refactor) mantendo verde.
  • Sem “pré-arquitetura”: eu não adiciono camadas porque “fica bonito”; eu adiciono porque um teste precisa.
  • Refatoração orientada a sinais:
    teste lento → limite errado; teste frágil → contrato implícito; duplicação → extração de regra comum.
Métrica mental: “Quanto esforço eu gasto para adicionar um novo teste do mesmo tipo?”
Se cai gradualmente, minha arquitetura está ajudando; se dispara, a arquitetura está atrapalhando.
Exemplo: caso de uso com porta (repositório) e regras no núcleo
JavaScript/TypeScript (ilustrativo)
type Email = string;

class DuplicateEmailError extends Error {
  constructor(email: Email){
    super(`Email já cadastrado: ${email}`);
    this.name = "DuplicateEmailError";
  }
}

interface UserRepository {
  existsByEmail(email: Email): Promise<boolean>;
  save(user: { id: string; email: Email }): Promise<void>;
}

interface IdGenerator {
  nextId(): string;
}

interface CreateUserInput {
  email: Email;
}

interface CreateUserOutput {
  id: string;
}

class CreateUser {
  constructor(
    private readonly repo: UserRepository,
    private readonly ids: IdGenerator
  ) {}

  async execute(input: CreateUserInput): Promise<CreateUserOutput> {
    const email = input.email;

    const exists = await this.repo.existsByEmail(email);
    if (exists) throw new DuplicateEmailError(email);

    const id = this.ids.nextId();
    await this.repo.save({ id, email });

    return { id };
  }
}

/**
 * Teste de comportamento (conceitual):
 * - Quando email existe: deve lançar DuplicateEmailError
 * - Quando email não existe: deve persistir e devolver id
 *
 * O teste conversa com o CreateUser e com portas (UserRepository/IdGenerator),
 * não com banco/HTTP.
 */

Quer continuar evoluindo sua arquitetura com TDD?

Se você gostou do enfoque em limites, contratos e refatoração guiada por testes,
leia também outros posts do yurideveloper.com para transformar TDD em um hábito de design — e não só de escrita de testes.

yurideveloper.com • Dominando a arquitetura de TDD com limites claros, portas explícitas e refatoração segura.



“`