🧪 Testes Unitários: Comandos vs Mocks em Go 🔗

Ao escrever testes unitários em Go, lidar com dependências externas como sistemas de arquivos, relógios do sistema ou comandos externos pode ser complicado. Este post explora três abordagens para resolver esse problema, mostrando ao final por que o Padrão Command oferece uma alternativa limpa e testável aos mocks complexos.

Vamos analisar um exemplo: uma função projetada para fazer benchmark de builds incrementais.

func Benchmark(binary string, sourcePath string, resultPath string, numBuilds int) error {
	if err := build(binary); err != nil {
		return err
	}
	source, err := read(sourcePath)
	if err != nil {
		return err
	}
	
	deltas := []time.Duration{}
	for i := 0; i < numBuilds; i++ {
		if err := write(sourcePath, edit(source, i)); err != nil {
			return err
		}
		
		start := now()
		if err := build(binary); err != nil {
			return err
		}
		
		delta := now().Sub(start)
		deltas = append(deltas, delta)
		
		if err := revert(sourcePath); err != nil {
			return err
		}
	}
	return write(resultPath, report(deltas))
}

📑 Índice 🔗


⚠️ O Problema 🔗

Esta função Benchmark faz várias coisas:

  1. Compila um binário.
  2. Lê um arquivo de código-fonte.
  3. Executa um loop para editar o código-fonte, medir o tempo de compilação e reverter o arquivo.
  4. Grava o relatório final.

Testar isso diretamente é difícil porque depende de quatro fatores externos:

Muitos desenvolvedores testariam apenas as funções independentes edit e report, deixando a orquestração principal sem testes. Veja como podemos melhorar o design para tornar todo o processo testável.


1️⃣ Abordagem 1: Mover o Código para o Main 🔗

Às vezes, você pode empurrar as dependências para quem chama a função (frequentemente o main).

func Benchmark(binary string, sourcePath string, source string, numBuilds int) (string, error) {
	// ... core loop logic remains ...
	// returning the report string instead of writing it
	return report(deltas), nil
}

func Main() error {
	// setup and initial build moved here
	source, _ := read(sourcePath)
	result, _ := Benchmark(binary, sourcePath, source, 3)
	return write(resultPath, result)
}

Veredito: Embora isso ajude, não é suficiente neste caso. O loop de Benchmark ainda precisa fundamentalmente interagir com o sistema de build, o relógio, o sistema de arquivos e o controle de versão durante cada iteração.


2️⃣ Abordagem 2: Injeção de Dependência (Mocks/Fakes) 🔗

A abordagem tradicional orientada a objetos é a Injeção de Dependência. Você define uma interface que representa todas as ações externas.

type Delegate interface {
	Build(binary string) error
	Read(path string) (string, error)
	Write(path string, content string) error
	Now() time.Time
	Revert(path string) error
}

type BenchmarkRunner struct {
	Delegate Delegate
}

func (b *BenchmarkRunner) Run(binary string, sourcePath string, resultPath string, numBuilds int) error {
	// ... implementation using b.Delegate.Build(), b.Delegate.Now(), etc ...
}

Veredito: Isso funciona bem e permite substituir por um delegate falso (fake) nos testes. No entanto, exige definir interfaces e implementações de mock para cada tipo de efeito colateral, o que pode levar ao inchaço de interfaces (interface bloat).


3️⃣ Abordagem 3: O Padrão Command 🔗

Em vez de definir interfaces específicas para cada ação, podemos tratar cada efeito colateral como um comando genérico a ser executado por um “Runner” central.

Primeiro, defina o que significa executar um comando:

type Runner interface {
	Run(cmd Command) (Result, error)
}

Em seguida, defina seus comandos específicos como structs simples (ou constantes):

type BuildCmd struct { Binary string }
type ReadCmd struct { Path string }
type WriteCmd struct { Path string; Content string }
type NowCmd struct {}
type RevertCmd struct { Path string }

Agora, refatore a lógica de benchmark para retornar os comandos que ela deseja executar ou passar o controle para um runner:

type BenchmarkProcess struct {
	runner Runner
}

func (p *BenchmarkProcess) Execute(binary, sourcePath, resultPath string, numBuilds int) error {
	_, err := p.runner.Run(BuildCmd{Binary: binary})
	if err != nil { return err }
	
	readRes, err := p.runner.Run(ReadCmd{Path: sourcePath})
	if err != nil { return err }
	source := readRes.(string) // Type assertion needed here

	// ... rest of the loop using p.runner.Run() ...
}

💡 Por Que Comandos São Melhores para Testes 🔗

O verdadeiro poder desse padrão brilha nos testes unitários. Você só precisa escrever um FakeRunner em vez de implementar interfaces complexas de mock para cada dependência.

O teste se torna uma lista simples de comandos esperados e respostas simuladas:

func TestBenchmark(t *testing.T) {
	// 1. Define the script: "When you see this command, return this result"
	fakeRunner := NewFakeRunner([]Step{
		{Cmd: BuildCmd{"binary"}, Err: nil},
		{Cmd: ReadCmd{"source_path"}, Res: "source code", Err: nil},
		{Cmd: WriteCmd{"source_path", "edited code"}, Err: nil},
		{Cmd: NowCmd{}, Res: time.Unix(0, 0), Err: nil},
		// ... and so on ...
	})

	p := BenchmarkProcess{runner: fakeRunner}
	err := p.Execute("binary", "source_path", "result_path", 1)
	
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	
	// 2. Verify the runner consumed all expected steps
	if !fakeRunner.IsDone() {
		t.Fatalf("did not execute all expected commands")
	}
}

🎯 Resumo 🔗

Ao transformar chamadas de função em estruturas de dados (Commands), você desacopla sua lógica de negócios de seus efeitos colaterais.


📚 Baixe Meu Ebook Gratuito 🔗

Baixe a edição offline selecionada de O Estilo de Vida do Kaue—um guia prático para otimizar 🍏 Saúde, 💰 Riqueza, ❤️ Amor e 🧘 Sentido.

O Estilo de Vida do Kaue

O Estilo de Vida do Kaue

📬 Assine Minha Newsletter 🔗

Receba notificações quando eu publicar novos ensaios, resumos de livros e guias práticos sobre como viver bem. Sem spam—cancele quando quiser.


💬 Comentários e Discussão 🔗

Tem alguma pergunta, perspectiva ou sugestão? Compartilhe suas ideias abaixo. Os comentários usam o GitHub Discussions (sem anúncios e sem spam)—clique em Entrar com o GitHub abaixo para comentar ou reagir (criar uma conta gratuita no GitHub leva menos de um minuto).



Atualizado em 11 de out. de 2026.

AVISO LEGAL: O conteúdo deste site tem finalidade exclusivamente informativa e educacional e não constitui aconselhamento médico, psicológico, financeiro, de investimentos, tributário ou jurídico. As ideias, a curadoria e as opiniões aqui apresentadas são exclusivamente minhas e não representam as do meu empregador. Os resumos de livros, palestras e artigos são comentários educacionais independentes e permanecem propriedade intelectual de seus respectivos autores. Ferramentas de IA podem ser utilizadas para auxiliar na edição, formatação e síntese; embora todos os esforços sejam feitos para garantir a precisão, erros podem ocorrer. PRIVACIDADE: Este site utiliza serviços de terceiros para inscrição opcional em newsletter (follow.it), comentários (giscus / GitHub Discussions) e mídia incorporada (YouTube), que podem processar endereços IP e cookies dos visitantes de acordo com suas respectivas políticas de privacidade.