Mocks e dublês de teste
Nesta aula, você aprenderá a usar mocks e dublês de teste em Go para isolar unidades de código, testar interações e simular dependências externas. Exploraremos interfaces para testabilidade, criação de mocks manuais, uso do pacote httptest para testar servidores HTTP e boas práticas essenciais.
Em testes de software, um dublê de teste (test double) é um objeto que substitui uma dependência real para permitir que você teste uma unidade de código de forma isolada. Em Go, a abordagem mais comum e idiomática para criar dublês é usar interfaces. Isso porque interfaces em Go são satisfeitas implicitamente: qualquer tipo que implemente os métodos necessários pode ser usado onde a interface é esperada. Isso facilita a criação de implementações falsas ou simuladas que podem ser controladas no teste.
Nesta aula, vamos nos concentrar em três tipos principais de dublês: stubs (que fornecem respostas pré-definidas), mocks (que verificam interações, como chamadas de métodos e argumentos) e fakes (implementações simplificadas que funcionam de verdade, mas de forma mais simples). Vamos ver como criar esses dublês manualmente e também como usar ferramentas do pacote padrão, como httptest, para testar servidores HTTP sem precisar de uma rede real.
Interfaces para testabilidade
O primeiro passo para tornar seu código testável é projetá-lo em torno de interfaces. Em vez de depender de tipos concretos (como um cliente HTTP real ou um banco de dados), você define uma interface que descreve apenas o comportamento necessário. Isso permite que, nos testes, você forneça uma implementação falsa que atenda à interface.
Por exemplo, suponha que você tenha uma função que envia e-mails. Em vez de depender diretamente de um pacote de e-mail, você define uma interface EmailSender com um método Send. Assim, no teste, você pode criar um mock que registra as chamadas e retorna um erro simulado se necessário.
Vejamos um exemplo simples. Primeiro, definimos a interface:
type EmailSender interface {
Send(to, subject, body string) error
}Agora, uma função que usa essa interface:
func NotifyUser(sender EmailSender, email string) error {
return sender.Send(email, "Bem-vindo", "Obrigado por se cadastrar!")
}Com essa abstração, podemos testar NotifyUser sem enviar e-mails reais. Vamos criar um mock manual na próxima seção.
Mocks manuais
Um mock manual é uma struct que implementa a interface e permite que você controle seu comportamento e registre as chamadas feitas. Em Go, isso é feito simplesmente definindo métodos que correspondem à interface e adicionando campos para armazenar informações, como argumentos recebidos ou respostas configuradas.
Vamos criar um mock para a interface EmailSender:
type MockEmailSender struct {
sentTo []string
sentSubject []string
sentBody []string
err error
}
func (m *MockEmailSender) Send(to, subject, body string) error {
m.sentTo = append(m.sentTo, to)
m.sentSubject = append(m.sentSubject, subject)
m.sentBody = append(m.sentBody, body)
return m.err
}
func (m *MockEmailSender) SetError(err error) {
m.err = err
}
func (m *MockEmailSender) SentCalls() int {
return len(m.sentTo)
}Agora, no teste, podemos verificar se a função NotifyUser chamou o método Send com os argumentos corretos:
func TestNotifyUser(t *testing.T) {
mock := &MockEmailSender{}
err := NotifyUser(mock, "user@example.com")
if err != nil {
t.Errorf("erro inesperado: %v", err)
}
if mock.SentCalls() != 1 {
t.Errorf("esperava 1 chamada, got %d", mock.SentCalls())
}
if mock.sentTo[0] != "user@example.com" {
t.Errorf("destinatário errado: %s", mock.sentTo[0])
}
}Esse padrão é simples e poderoso. Você pode estender o mock para retornar erros específicos, simular delays, ou até mesmo implementar um fake que realmente envie o e-mail para um servidor de teste. A vantagem é que você não precisa de bibliotecas externas; tudo é feito com tipos e métodos Go.
httptest
Quando você precisa testar código que faz requisições HTTP, o pacote net/http/httptest é essencial. Ele fornece utilitários para criar servidores de teste e para gravar respostas. Você pode testar handlers HTTP sem abrir uma porta real, usando httptest.NewServer ou httptest.NewRecorder.
Vamos ver um exemplo. Suponha que você tenha um handler que retorna um JSON:
func UserHandler(w http.ResponseWriter, r *http.Request) {
user := map[string]string{"name": "Alice", "email": "alice@example.com"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}Para testar esse handler, você pode usar httptest.NewRecorder para capturar a resposta:
func TestUserHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/user", nil)
rec := httptest.NewRecorder()
UserHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status code esperado 200, got %d", rec.Code)
}
expected := `{"email":"alice@example.com","name":"Alice"}`
if rec.Body.String() != expected {
t.Errorf("corpo inesperado: %s", rec.Body.String())
}
}Além disso, httptest.NewServer cria um servidor real em uma porta aleatória, o que é útil para testar clientes HTTP. Por exemplo, se você tem um cliente que chama uma API externa, pode substituir a URL base pelo servidor de teste:
func TestFetchUser(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"name":"Alice"}`)
}))
defer server.Close()
client := &http.Client{}
resp, err := client.Get(server.URL + "/user")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
// ler e verificar o corpo
}O httptest também fornece httptest.NewTLSServer para testes com HTTPS e permite verificar headers, status e corpo facilmente.
Boas práticas
Ao usar mocks e dublês, é importante seguir algumas boas práticas para manter seus testes confiáveis e fáceis de manter.
- Prefira interfaces pequenas e focadas: Quanto menor a interface, mais fácil é criar um mock. Evite interfaces gigantes com muitos métodos.
- Use mocks para verificar interações, não para implementar lógica: Mocks devem apenas registrar chamadas e retornar valores configurados. Não coloque lógica complexa dentro deles.
- Nomeie os mocks de forma clara: Por exemplo,
MockEmailSenderouStubUserRepository. Isso ajuda na legibilidade. - Evite over-mocking: Se você está testando uma função simples que apenas chama outra, talvez o teste não agregue valor. Concentre-se em testar comportamentos importantes.
- Use fakes para integrações simples: Em vez de mockar um repositório de banco de dados, você pode criar um fake que armazena dados em memória. Isso é mais realista e ainda isolado.
- Combine mocks com testes de integração: Mocks são ótimos para testes unitários, mas não substituem testes de integração com componentes reais.
- Mantenha os mocks atualizados: Se a interface mudar, os mocks quebram. Isso é bom, pois sinaliza que você precisa atualizar os testes.
Além disso, considere usar bibliotecas de geração de mocks, como mockery ou gomock, para gerar mocks automaticamente a partir de interfaces. Elas economizam tempo e padronizam o código. No entanto, entender como criar mocks manualmente é fundamental para dominar os conceitos.
Referências
- Effective Go - Documentação oficial sobre boas práticas em Go.
- Pacote httptest - Documentação oficial
- Pacote testing - Documentação oficial
- gomock - Biblioteca de mocks
- mockery - Gerador de mocks
- Mocks Aren't Stubs - Martin Fowler
- Table-driven tests - Wiki do Go
Exercícios
- Crie uma interface
UserRepositorycom métodosFindByID(id int) (User, error)eSave(user User) error. Implemente um mock manual que registre as chamadas e retorne valores configurados. Escreva um teste para uma função que usa essa interface. - Usando
httptest.NewRecorder, escreva um teste para um handler que retorna um JSON com um campostatusigual a"ok". Verifique o status code e o corpo da resposta. - Implemente um fake para a interface
EmailSenderque armazena os e-mails em um slice e não envia nada. Use-o em um teste para verificar que a funçãoNotifyUserenvia o e-mail com o assunto correto. - Escreva um teste para um cliente HTTP que consome uma API. Use
httptest.NewServerpara simular a API e verifique se o cliente trata uma resposta 404 corretamente. - Refatore o mock manual do exercício 1 para usar a biblioteca
gomock(oumockery) e gere o mock automaticamente. Ajuste o teste para usar o mock gerado.
type User struct {
ID int
Name string
}
type UserRepository interface {
FindByID(id int) (User, error)
Save(user User) error
}
type MockUserRepository struct {
users map[int]User
err error
}
func (m *MockUserRepository) FindByID(id int) (User, error) {
if m.err != nil {
return User{}, m.err
}
user, exists := m.users[id]
if !exists {
return User{}, fmt.Errorf("usuário não encontrado")
}
return user, nil
}
func (m *MockUserRepository) Save(user User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}
func TestGetUser(t *testing.T) {
mock := &MockUserRepository{
users: map[int]User{1: {ID: 1, Name: "Alice"}},
}
user, err := mock.FindByID(1)
if err != nil {
t.Fatal(err)
}
if user.Name != "Alice" {
t.Errorf("nome inesperado: %s", user.Name)
}
}func StatusHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"status":"ok"}`)
}
func TestStatusHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/status", nil)
rec := httptest.NewRecorder()
StatusHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status code esperado 200, got %d", rec.Code)
}
expected := `{"status":"ok"}`
if rec.Body.String() != expected {
t.Errorf("corpo inesperado: %s", rec.Body.String())
}
}type FakeEmailSender struct {
emails []string
}
func (f *FakeEmailSender) Send(to, subject, body string) error {
f.emails = append(f.emails, to+": "+subject)
return nil
}
func TestNotifyUserFake(t *testing.T) {
fake := &FakeEmailSender{}
err := NotifyUser(fake, "user@example.com")
if err != nil {
t.Fatal(err)
}
if len(fake.emails) != 1 {
t.Fatalf("esperava 1 email, got %d", len(fake.emails))
}
if fake.emails[0] != "user@example.com: Bem-vindo" {
t.Errorf("email inesperado: %s", fake.emails[0])
}
}func TestClient404(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
}))
defer server.Close()
resp, err := http.Get(server.URL + "/missing")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNotFound {
t.Errorf("status code esperado 404, got %d", resp.StatusCode)
}
}// Usando gomock: gere o mock com mockgen e use no teste.
// Exemplo de teste com mock gerado:
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := NewMockUserRepository(ctrl)
mockRepo.EXPECT().FindByID(1).Return(User{ID: 1, Name: "Alice"}, nil)
user, err := mockRepo.FindByID(1)
if err != nil {
t.Fatal(err)
}
if user.Name != "Alice" {
t.Errorf("nome inesperado: %s", user.Name)
}