Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

API de Tarefas Testável em Go

Projeto irmão do api-tarefas-mvc. Os dois oferecem o mesmo CRUD com PostgreSQL, mas este separa regras e persistência para demonstrar abstração, interfaces e testes unitários isolados.

Arquitetura usada

Requisição HTTP
       ↓
Controller → Service → Repository → PostgreSQL
       ↓
     View JSON
  • internal/model: entidade e erros conhecidos pela aplicação;
  • internal/repository: implementação concreta da persistência PostgreSQL;
  • internal/service: contrato do Repository e regras de negócio;
  • internal/controller: protocolo HTTP e escolha dos status;
  • internal/view: formato e serialização do JSON;
  • cmd/api: conexão e montagem explícita das dependências.

O acesso ao PostgreSQL usa pgxpool diretamente. A API recebe várias requisições concorrentes, portanto o pool fornece e reutiliza conexões sem passar pelo adaptador database/sql:

PostgresTaskRepository → pgxpool.Pool → PostgreSQL

O identificador mantém um tipo adequado em cada fronteira:

URL/JSON string → Controller uuid.Parse → Model/Service/Repository uuid.UUID
                                              ↓
                              PostgreSQL UUID DEFAULT uuidv7()

O PostgreSQL 18 gera o UUID versão 7. O pacote github.com/google/uuid representa o identificador no código Go, e a View volta a serializá-lo como string no JSON.

Este é um MVC organizado em camadas, não um MVC puro. Service e Repository acrescentam responsabilidades que não existiam no projeto mais simples.

Por que as interfaces mudam os testes

TaskService depende da interface TaskRepository, declarada no próprio pacote service, e não de PostgresTaskRepository. Em Go, a interface normalmente fica perto de quem a consome. O tipo concreto satisfaz o contrato implicitamente, sem uma declaração como implements.

Por isso, o teste entrega um Repository falso e verifica as regras sem Docker, rede ou banco.

O Controller segue a mesma ideia: declara a interface TaskService com os casos de uso de que precisa. Seu teste usa uma Service falsa e observa somente leitura da requisição, status e resposta JSON.

Partes reais no cenário Dependência substituída Nome usado neste projeto
Service Repository falso Teste unitário
Controller chamado diretamente Service falsa Teste unitário
Router + Controller + View com NewRecorder Service falsa Teste de componente HTTP sem socket
Socket HTTP + Router + Controller + View Service falsa Integração HTTP local de componente
Repository + PostgreSQL Nenhuma Integração de persistência
Socket HTTP + todas as camadas + PostgreSQL Nenhuma Integração full-stack local

As interfaces não tornam o PostgreSQL desnecessário. Elas permitem escolher quando o teste precisa dele. Essa separação também tem um custo: mais tipos, arquivos e montagem. A abstração vale a pena quando existe uma fronteira que realmente precisa ser isolada. A integração full-stack ainda não é chamada de ponta a ponta porque o teste monta a aplicação dentro do próprio processo; um E2E executaria o binário como sistema externo e o observaria pela fronteira pública.

Organização dos testes

Em Go, os testes unitários normalmente ficam ao lado do código que verificam, com o sufixo _test.go. Não é necessário criar uma árvore tests/unit como é comum em alguns projetos Python. Isso permite testar cada pacote diretamente e, quando necessário, usar o pacote externo nome_test para observar somente a API exportada.

Este projeto separa os testes mais caros por build tag:

internal/model/*_test.go       testes unitários do Model
internal/service/*_test.go     testes unitários da Service e doubles
internal/controller/*_test.go  testes unitários e de componente HTTP
integration/*_test.go          integração real; exige -tags=integration
integration/testdata/*.sql     fixtures/seed SQL
examples/complexity/*          exemplo isolado de benchmark e Big O

testdata é uma convenção reconhecida pelas ferramentas Go: seu conteúdo pode ser lido pelos testes, mas o diretório não vira um pacote da aplicação.

Executar os testes unitários

Os testes unitários não precisam do PostgreSQL:

go test ./... -v

Para medir cobertura:

go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out

Os arquivos mostram os três conceitos do módulo:

  • internal/service/task_test.go: fake de Repository, table-driven tests e subtests;
  • internal/controller/task_test.go: fake de Service e teste HTTP com httptest;
  • a saída de cobertura mostra quais camadas ainda precisam de testes de integração.

Executar os testes de integração

Inicie somente o PostgreSQL 18 dedicado aos testes e aguarde o healthcheck:

docker compose --profile test up -d --wait postgres-test

Execute somente a suíte de integração:

go test -tags=integration -count=1 -v ./integration

O endereço pode ser substituído quando necessário:

TEST_DATABASE_URL='postgres://postgres:postgres@localhost:5434/tarefas_test?sslmode=disable' \
  go test -tags=integration -count=1 -v ./integration

O serviço postgres-test usa porta, database e volume diferentes do PostgreSQL da aplicação. O helper também recusa qualquer URL cujo database não termine em _test. Cada teste executa:

SETUP:   aplica database/init.sql → TRUNCATE → executa o seed
TESTE:   usa Repository real ou aplicação HTTP completa
CLEANUP: TRUNCATE → fecha o pool de conexões

O seed está em integration/testdata/seed_tasks.sql e fornece dois registros determinísticos. Não usamos rollback por teste no fluxo HTTP porque o servidor e o Repository podem obter conexões diferentes do pool; setup e cleanup explícitos tornam o isolamento visível e repetível.

Para executar testes rápidos e integração em um único comando:

go test -tags=integration -count=1 ./...

Executar a API

Este projeto usa PostgreSQL 18.4 e uma porta diferente do exemplo simples para que ambos possam ser executados ao mesmo tempo. Inicie o banco na porta local 5433 e espere o healthcheck ficar saudável:

docker compose up -d --wait

Na imagem oficial do PostgreSQL 18, PGDATA é /var/lib/postgresql/18/docker e o volume deve ser montado em /var/lib/postgresql. Esse caminho é diferente do usado pelas imagens 17 e anteriores. O compose.yaml usa um volume chamado postgres18_data para não reaproveitar acidentalmente dados criados por outra versão principal.

Depois inicie a API na porta 8081:

go run ./cmd/api

A conexão padrão pode ser substituída:

DATABASE_URL="postgres://usuario:senha@localhost:5433/tarefas?sslmode=disable" go run ./cmd/api

Para apenas parar e remover os containers, preservando o volume:

docker compose down

Os arquivos em /docker-entrypoint-initdb.d só são executados quando o volume está vazio. Se a intenção for apagar deliberadamente os dados locais e reconstruir o banco com database/init.sql:

docker compose down -v

Esse último comando remove o volume local do projeto. Não o use para uma instância que contenha dados que precisam ser preservados.

Endpoints

Método Rota Descrição
POST /tarefas Cria uma tarefa pendente
GET /tarefas Lista as tarefas
GET /tarefas/{id} Busca uma tarefa
PUT /tarefas/{id} Atualiza título e conclusão
DELETE /tarefas/{id} Exclui uma tarefa

Exemplo de criação:

{
  "titulo": "Estudar testes em Go"
}

Exemplo de resposta (id é string JSON):

{
  "id": "019535d9-3df7-7001-8000-000000000001",
  "titulo": "Estudar testes em Go",
  "concluida": false
}

Nas rotas com {id}, o Controller rejeita texto que não possa ser convertido em um UUID válido antes de chamar a Service. No banco, database/init.sql usa:

id UUID PRIMARY KEY DEFAULT uuidv7()

Roteiro de leitura

  1. Comece por cmd/api/main.go e identifique a montagem das dependências.
  2. Leia a entidade pequena em internal/model/task.go.
  3. Veja a interface TaskRepository e as regras em internal/service/task.go.
  4. Observe que o Repository concreto não importa nem conhece essa interface.
  5. Compare essas regras com o fake em internal/service/task_test.go.
  6. Veja a interface TaskService declarada pelo Controller.
  7. Compare o Controller com seu teste em internal/controller/task_test.go.
  8. Por último, leia a implementação PostgreSQL do Repository.

Comparação com o projeto sem abstração

Projeto simples Projeto testável
Model mistura regras e SQL Service contém regras; Repository contém SQL
Controller depende do Model concreto Controller depende da interface da Service
CRUD exige PostgreSQL no teste Regras e HTTP podem ser testados sem PostgreSQL
Menos código e conceitos Mais isolamento e mais tipos

O objetivo não é afirmar que mais camadas são sempre melhores. Os dois projetos permitem discutir quando a simplicidade é suficiente e quando o isolamento das dependências justifica uma abstração.

Exemplos de doubles, HTTP, integração e benchmarks

Os arquivos abaixo demonstram mocks, testes HTTP, integração local e benchmarks:

  • internal/service/task_doubles_test.go: Stub + Spy injetado na Service;
  • internal/service/task_fake_mock_test.go: Fake funcional e Mock clássico manual injetados na Service;
  • internal/controller/router_test.go: testes POST e GET que atravessam Router e Controller;
  • internal/controller/router_server_test.go: integração HTTP local de componente com httptest.NewServer e Service falsa;
  • internal/repository/task_integration_test.go: integração com PostgreSQL local;
  • integration/setup_test.go: conexão fail-closed, setup, seed e cleanup compartilhados;
  • integration/repository_test.go: seed e CRUD completo do Repository com PostgreSQL real;
  • integration/http_test.go: integração full-stack local de todas as camadas;
  • integration/testdata/seed_tasks.sql: fixture SQL determinística;
  • examples/complexity: teste e benchmark comparando O(1), O(n), O(n²) e O(n³).

Use os comandos abaixo para os exercícios e exemplos específicos:

# Stub + Spy na Service
go test ./internal/service \
  -run TestTaskServiceCreateWithStubAndSpy -count=1 -v

# Fake funcional e Mock clássico manual
go test ./internal/service \
  -run '^(TestTaskServiceLifecycleWithFake|TestTaskServiceCreateWithMock)$' \
  -count=1 -v

# Testes HTTP POST e GET através do Router
go test ./internal/controller \
  -run '^TestRouter(Create|Get)Task$' -count=1 -v

# Integração local, com postgres-test já iniciado
TEST_DATABASE_URL='postgres://postgres:postgres@localhost:5434/tarefas_test?sslmode=disable' \
  go test -tags=integration ./internal/repository \
  -run TestPostgresTaskRepositoryCreateAndGet -count=1 -v

# Suíte ativa: seed, Repository, constraints e HTTP full-stack
go test -tags=integration -count=1 -v ./integration

# Comparação curta de O(1), O(n), O(n²) e O(n³)
go test -run '^$' -bench '^BenchmarkO' -benchmem ./examples/complexity

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages