Introdução: Por que testes avançados são um must have em Go
Go (Golang) é famoso por sua simplicidade e ferramentas integradas para testes. No entanto, quando o projeto cresce além do "Hello, World", o padrão go test deixa de ser suficiente. Em nível avançado, os desenvolvedores enfrentam questões: como isolar testes corretamente, como testar a interação com o banco de dados, como simular serviços externos e como organizar uma verificação ponta a ponta de toda a aplicação.
Neste artigo, abordaremos três níveis de teste — Unit, Integration e E2E — com exemplos da prática real. Você aprenderá a usar mocks, contêineres para testes de integração e como construir um pipeline confiável para CI/CD. O material é voltado para desenvolvedores que já conhecem os fundamentos do Go e desejam elevar a qualidade do seu código a um novo patamar.
1. Testes unitários: isolamento, mocks e o padrão Table-Driven Tests
Testes unitários verificam o funcionamento de um único módulo (função, método) em completo isolamento de dependências externas. Em Go, para isso, frequentemente se usam interfaces e bibliotecas de mock, como testify/mock ou gomock.
1.1. Interfaces como base para testes
Para substituir um serviço real por um mock, o código deve depender de uma interface, e não de uma implementação concreta. Vejamos um exemplo de um serviço para envio de notificações.
// notifier.gotype Notifier interface { Send(to string, message string) error}
type UserService struct { Notifier Notifier}
func (s *UserService) NotifyUser(userID string) error { // lógica para obter o email do usuário email := userID + "@example.com" return s.Notifier.Send(email, "Welcome!") }1.2. Criando um mock com testify/mock
Instale a biblioteca: go get github.com/stretchr/testify.
// notifier_mock.go (no pacote user_test)type NotifierMock struct { mock.Mock}
func (m *NotifierMock) Send(to string, message string) error { args := m.Called(to, message) return args.Error(0)}1.3. Table-Driven Tests
Este é o padrão padrão em Go para testar múltiplos casos. Ele melhora a legibilidade e simplifica a adição de novos testes.
// user_service_test.gofunc TestNotifyUser(t *testing.T) { tests := []struct { name string userID string mockError error expectErr bool }{ {"success", "user1", nil, false}, {"fail_send", "user2", errors.New("send error"), true}, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockNotifier := new(NotifierMock) service := &UserService{Notifier: mockNotifier}
mockNotifier.On("Send", tt.userID+"@example.com", "Welcome!").Return(tt.mockError)
err := service.NotifyUser(tt.userID) if tt.expectErr { assert.Error(t, err) } else { assert.NoError(t, err) } mockNotifier.AssertExpectations(t) }) }}Best practice: Use t.Run para subtestes — isso permite executar cada caso separadamente e ver qual deles falhou.
2. Testes de integração: trabalho com BD e serviços externos
Testes de integração verificam a interação de vários componentes: código Go com banco de dados, servidor API com cache, etc. O principal desafio é gerenciar o estado do ambiente. É aqui que entram os Testcontainers.
2.1. Testcontainers para Go
Testcontainers permitem iniciar contêineres Docker diretamente dos testes. Isso fornece um ambiente limpo e isolado para cada execução.
Instalação: go get github.com/testcontainers/testcontainers-go