Mocking

Você pode encontrar todos os códigos para esse capítulo aqui

Te pediram para criar um programa que conta a partir de 3, imprimindo cada número em uma linha nova (com um segundo de intervalo entre cada uma) e quando chega a zero, imprime "Vai!" e sai.

3
2
1
Vai!

Vamos resolver isso escrevendo uma função chamada Contagem que vamos colocar dentro de um programa main e se parecer com algo assim:

package main
func main() {
Contagem()
}

Apesar de ser um programa simples, para testá-lo completamente vamos precisar, como de costume, de uma abordagem iterativa e orientada a testes.

Mas o que quero dizer com iterativa? Precisamos ter certeza de que tomamos os menores passos que pudermos para ter um software útil.

Não queremos passar muito tempo com código que vai funcionar hora ou outra após alguma implementação mirabolante, porque é assim que os desenvolvedores caem em armadilhas. É importante ser capaz de dividir os requerimentos da menor forma que conseguir para você ter um software funcionando.

Podemos separar essa tarefa da seguinte forma:

  • Imprimir 3

  • Imprimir de 3 para Vai!

  • Esperar um segundo entre cada linha

Escreva o teste primeiro

Nosso software precisa imprimir para a saída. Vimos como podemos usar a injeção de dependência para facilitar nosso teste na seção anterior.

func TestContagem(t *testing.T) {
buffer := &bytes.Buffer{}
Contagem(buffer)
resultado := buffer.String()
esperado := "3"
if resultado != esperado {
t.Errorf("resultado '%s', esperado '%s'", resultado, esperado)
}
}

Se tiver dúvidas sobre o buffer, leia a seção anterior novamente.

Sabemos que nossa função Contagem precisa escrever dados em algum lugar e o io.Writer é a forma de capturarmos essa saída como uma interface em Go.

  • Na main, vamos enviar o os.Stdout como parâmetro para nossos usuários verem a contagem regressiva impressa no terminal.

  • No teste, vamos enviar o bytes.Buffer como parâmetro para que nossos testes possam capturar que dado está sendo gerado.

Execute o teste

./contagem_test.go:11:2: undefined: Contagem

indefinido: Contagem

Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado

Defina Contagem:

func Contagem() {}

Tente novamente:

./contagem_test.go:11:11: too many arguments in call to Countdown
have (*bytes.Buffer)
want ()

argumentos demais na chamada para Contagem

O compilador está te dizendo como a assinatura da função deve ser, então é só atualizá-la.

func Contagem(saida *bytes.Buffer) {}

contagem_test.go:17: resultado '', esperado '3'

Perfeito!

Escreva código o suficiente para fazer o teste passar

func Contagem(saida *bytes.Buffer) {
fmt.Fprint(saida, "3")
}

Estamos usando fmt.Fprint, o que significa que ele recebe um io.Writer (como *bytes.Buffer) e envia uma string para ele. O teste deve passar.

Refatoração

Agora sabemos que, apesar do *bytes.Buffer funcionar, seria melhor ter uma interface de propósito geral ao invés disso.

func Contagem(saida io.Writer) {
fmt.Fprint(saida, "3")
}

Execute os testes novamente e eles devem passar.

Só para finalizar, vamos colocar nossa função dentro da main para que possamos executar o software para nos assegurarmos de que estamos progredindo.

package main
import (
"fmt"
"io"
"os"
)
func Contagem(saida io.Writer) {
fmt.Fprint(saida, "3")
}
func main() {
Contagem(os.Stdout)
}

Execute o programa e surpreenda-se com seu trabalho.

Apesar de parecer simples, essa é a abordagem que recomendo para qualquer projeto. Escolher uma pequena parte da funcionalidade e fazê-la funcionar do começo ao fim com apoio de testes.

Depois, precisamos fazer o software imprimir 2, 1 e então "Vai!".

Escreva o teste primeiro

Após investirmos tempo e esforço para fazer o principal funcionar, podemos iterar nossa solução com segurança e de forma simples. Não vamos mais precisar parar e executar o programa novamente para ter confiança de que ele está funcionando, desde que a lógica esteja testada.

func TestContagem(t *testing.T) {
buffer := &bytes.Buffer{}
Contagem(buffer)
resultado := buffer.String()
esperado := `3
2
1
Vai!`
if resultado != esperado {
t.Errorf("resultado '%s', esperado '%s'", resultado, esperado)
}
}

A sintaxe de aspas simples é outra forma de criar uma string, mas te permite colocar coisas como linhas novas, o que é perfeito para nosso teste.

Execute o teste

contagem_test.go:21: resultado '3', esperado '3
2
1
Vai!'

Escreva código o suficiente para fazer o teste passar

func Contagem(saida io.Writer) {
for i := 3; i > 0; i-- {
fmt.Fprintln(saida, i)
}
fmt.Fprint(saida, "Go!")
}

Usamos um laço for fazendo contagem regressiva com i-- e depois fmt.Fprintln para imprimir a saida com nosso número seguro por um caracter de nova linha. Finalmente, usamos o fmt.Fprint para enviar "Vai!" no final.

Refatoração

Não há muito para refatorar além de transformar alguns valores mágicos em constantes com nomes descritivos.

const ultimaPalavra = "Go!"
const inicioContagem = 3
func Contagem(saida io.Writer) {
for i := inicioContagem; i > 0; i-- {
fmt.Fprintln(saida, i)
}
fmt.Fprint(saida, ultimaPalavra)
}

Se executar o programa agora, você deve obter a saída desejada, mas não tem uma contagem regressiva dramática com as pausas de 1 segundo.

Go te permite obter isso com time.Sleep. Tente adicionar essa função ao seu código.

func Contagem(saida io.Writer) {
for i := inicioContagem; i > 0; i-- {
time.Sleep(1 * time.Second)
fmt.Fprintln(saida, i)
}
time.Sleep(1 * time.Second)
fmt.Fprint(saida, ultimaPalavra)
}

Se você executar o programa, ele funciona conforme esperado.

Mock

Os testes ainda vão passar e o software funciona como planejado, mas temos alguns problemas:

  • Nossos testes levam 4 segundos para rodar.

    • Todo conteúdo gerado sobre desenvolvimento de software enfatiza a importância de loops de feedback rápidos.

    • Testes lentos arruinam a produtividade do desenvolvedor.

    • Imagine se os requerimentos ficam mais sofisticados, gerando a necessidade de mais testes. É viável adicionar 4s para cada teste novo de Contagem?

  • Não testamos uma propriedade importante da nossa função.

Temos uma dependência no Sleep que precisamos extrair para podermos controlá-la nos nossos testes.

Se conseguirmos mockar o time.Sleep, podemos usar a injeção de dependências para usá-lo ao invés de um time.Sleep "de verdade", e então podemos verificar as chamadas para certificar de que estão corretas.

Escreva o teste primeiro

Vamos definir nossa dependência como uma interface. Isso nos permite usar um Sleeper de verdade em main e um sleeper spy nos nossos testes. Usar uma interface na nossa função Contagem é essencial para isso e dá certa flexibilidade à função que a chamar.

type Sleeper interface {
Sleep()
}

Tomei uma decisão de design que nossa função Contagem não seria responsável por quanto tempo o sleep leva. Isso simplifica um pouco nosso código, pelo menos por enquanto, e significa que um usuário da nossa função pode configurar a duração desse tempo como preferir.

Agora precisamos criar um mock disso para usarmos nos nossos testes.

type SleeperSpy struct {
Chamadas int
}
func (s *SleeperSpy) Sleep() {
s.Chamadas++
}

Spies (espiões) são um tipo de mock em que podemos gravar como uma dependência é usada. Eles podem gravar os argumentos definidos, quantas vezes são usados etc. No nosso caso, vamos manter o controle de quantas vezes Sleep() é chamada para verificá-la no nosso teste.

Atualize os testes para injetar uma dependência no nosso Espião e verifique se o sleep foi chamado 4 vezes.

func TestContagem(t *testing.T) {
buffer := &bytes.Buffer{}
sleeperSpy := &SleeperSpy{}
Contagem(buffer, sleeperSpy)
resultado := buffer.String()
esperado := `3
2
1
Vai!`
if resultado != esperado {
t.Errorf("resultado '%s', esperado '%s'", resultado, esperado)
}
if sleeperSpy.Chamadas != 4 {
t.Errorf("não houve chamadas suficientes do sleeper, esperado 4, resultado %d", sleeperSpy.Chamadas)
}
}

Execute o teste

too many arguments in call to Contagem
have (*bytes.Buffer, *SpySleeper)
want (io.Writer)

Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado

Precisamos atualizar a Contagem para aceitar nosso Sleeper:

func Contagem(saida io.Writer, sleeper Sleeper) {
for i := inicioContagem; i > 0; i-- {
time.Sleep(1 * time.Second)
fmt.Fprintln(saida, i)
}
time.Sleep(1 * time.Second)
fmt.Fprint(saida, ultimaPalavra)
}

Se tentar novamente, nossa main não vai mais compilar pelo mesmo motivo:

./main.go:26:11: not enough arguments in call to Contagem
have (*os.File)
want (io.Writer, Sleeper)

Vamos criar um sleeper de verdade que implementa a interface que precisamos:

type SleeperPadrao struct {}
func (d *SleeperPadrao) Sleep() {
time.Sleep(1 * time.Second)
}

Podemos usá-lo na nossa aplicação real, como:

func main() {
sleeper := &SleeperPadrao{}
Contagem(os.Stdout, sleeper)
}

Escreva código o suficiente para fazer o teste passar

Agora o teste está compilando, mas não passando. Isso acontece porque ainda estamos chamando o time.Sleep ao invés da injetada. Vamos arrumar isso.

func Contagem(saida io.Writer, sleeper Sleeper) {
for i := inicioContagem; i > 0; i-- {
sleeper.Sleep()
fmt.Fprintln(saida, i)
}
sleeper.Sleep()
fmt.Fprint(saida, ultimaPalavra)
}

O teste deve passar sem levar 4 segundos.

Ainda temos alguns problemas

Ainda há outra propriedade importante que não estamos testando.

A Contagem deve ter uma pausa para cada impressão, como por exemplo:

  • Pausa

  • Imprime N

  • Pausa

  • Imprime N-1

  • Pausa

  • Imprime Vai!

  • etc

Nossa alteração mais recente só verifica se o software teve 4 pausas, mas essas pausas poderiam ocorrer fora de ordem.

Quando escrevemos testes, se não estiver confiante de que seus testes estão te dando confiança o suficiente, quebre-o (mas certifique-se de que você salvou suas alterações antes)! Mude o código para o seguinte:

func Contagem(saida io.Writer, sleeper Sleeper) {
for i := inicioContagem; i > 0; i-- {
sleeper.Pausa()
fmt.Fprintln(saida, i)
}
for i := inicioContagem; i > 0; i-- {
fmt.Fprintln(saida, i)
}
sleeper.Pausa()
fmt.Fprint(saida, ultimaPalavra)
}

Se executar seus testes, eles ainda vão passar, apesar da implementação estar errada.

Vamos usar o spy novamente com um novo teste para verificar se a ordem das operações está correta.

Temos duas dependências diferentes e queremos gravar todas as operações delas em uma lista. Logo, vamos criar um spy para ambas.

type SpyContagemOperacoes struct {
Chamadas []string
}
func (s *SpyContagemOperacoes) Pausa() {
s.Chamadas = append(s.Chamadas, pausa)
}
func (s *SpyContagemOperacoes) Write(p []byte) (n int, err error) {
s.Chamadas = append(s.Chamadas, escrita)
return
}
const escrita = "escrita"
const pausa = "pausa"

Nosso SpyContagemOperacoes implementa tanto o io.Writer quanto o Sleeper, gravando cada chamada em um slice. Nesse teste, temos preocupação apenas na ordem das operações, então apenas gravá-las em uma lista de operações nomeadas é suficiente.

Agora podemos adicionar um subteste no nosso conjunto de testes.

t.Run("pausa antes de cada impressão", func(t *testing.T) {
spyImpressoraSleep := &SpyContagemOperacoes{}
Contagem(spyImpressoraSleep, spyImpressoraSleep)
esperado := []string{
pausa,
escrita,
pausa,
escrita,
pausa,
escrita,
pausa,
escrita,
}
if !reflect.DeepEqual(esperado, spyImpressoraSleep.Chamadas) {
t.Errorf("esperado %v chamadas, resultado %v", esperado, spyImpressoraSleep.Chamadas)
}
})

Esse teste deve falhar. Volte o código que quebramos para a versão correta e agora o novo teste deve passar.

Agora temos dois spies no Sleeper. O próximo passo é refatorar nosso teste para que um teste o que está sendo impresso e o outro se certifique de que estamos pausando entre as impressões. Por fim, podemos apagar nosso primeiro spy, já que não é mais utilizado.

func TestContagem(t *testing.T) {
t.Run("imprime 3 até Vai!", func(t *testing.T) {
buffer := &bytes.Buffer{}
Contagem(buffer, &SpyContagemOperacoes{})
resultado := buffer.String()
esperado := `3
2
1
Vai!`
if resultado != esperado {
t.Errorf("resultado '%s', esperado '%s'", resultado, esperado)
}
})
t.Run("pausa antes de cada impressão", func(t *testing.T) {
spyImpressoraSleep := &SpyContagemOperacoes{}
Contagem(spyImpressoraSleep, spyImpressoraSleep)
esperado := []string{
pausa,
escrita,
pausa,
escrita,
pausa,
escrita,
pausa,
escrita,
}
if !reflect.DeepEqual(esperado, spyImpressoraSleep.Chamadas) {
t.Errorf("esperado %v chamadas, resultado %v", esperado, spyImpressoraSleep.Chamadas)
}
})
}

Agora temos nossa função e suas duas propriedades testadas adequadamente.

Extendendo o Sleeper para se tornar configurável

Uma funcionalidade legal seria o Sleeper ser configurável.

Escreva o teste primeiro

Agora vamos criar um novo tipo para SleeperConfiguravel que aceita o que precisamos para configuração e teste.

type SleeperConfiguravel struct {
duracao time.Duration
pausa func(time.Duration)
}

Estamos usando a duracao para configurar o tempo de pausa e pausa como forma de passar uma função de pausa. A assinatura de sleep é a mesma de time.Sleep, nos permitindo usar time.Sleep na nossa implementação real e um spy nos nossos testes.

type TempoSpy struct {
duracaoPausa time.Duration
}
func (t *TempoSpy) Pausa(duracao time.Duration) {
t.duracaoPausa = duracao
}

Definindo nosso spy, podemos criar um novo teste para o sleeper configurável.

func TestSleeperConfiguravel(t *testing.T) {
tempoPausa := 5 * time.Second
tempoSpy := &TempoSpy{}
sleeper := SleeperConfiguravel{tempoPausa, tempoSpy.Pausa}
sleeper.Pausa()
if tempoSpy.duracaoPausa != tempoPausa {
t.Errorf("deveria ter pausado por %v, mas pausou por %v", tempoPausa, tempoSpy.duracaoPausa)
}
}

Não há nada de novo nesse teste e seu funcionamento é bem semelhante aos testes com mock anteriores.

Execute o teste

sleeper.Pausa undefined (type SleeperConfiguravel has no field or method Pausa, but does have pausa)

sleeper.Pausa não definido (tipo SleeperConfiguravel não tem campo ou método Pausa, mas tem o método sleep

Você deve ver uma mensagem de erro bem clara indicando que não temos um método Pausa criado no nosso SleeperConfiguravel.

Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado

func (c *SleeperConfiguravel) Pausa() {
}

Com nossa nova função Pausa implementada, ainda há um teste falhando.

contagem_test.go:56: deveria ter pausado por 5s, mas pausou por 0s

Escreva código o suficiente para fazer o teste passar

Tudo o que precisamos fazer agora é implementar a função Pausa para o SleeperConfiguravel.

func (s *SleeperConfiguravel) Pausa() {
s.pausa(s.duracao)
}

Com essa mudança, todos os testes devem voltar a passar.

Limpeza e refatoração

A última coisa que precisamos fazer é de fato usar nosso SleeperConfiguravel na função main.

func main() {
sleeper := &SleeperConfiguravel{1 * time.Second, time.Sleep}
Contagem(os.Stdout, sleeper)
}

Se executarmos os testes e o programa manualmente, podemos ver que todo o comportamento permanece o mesmo.

Já que estamos usando o SleeperConfiguravel, é seguro deletar o SleeperPadrao.

Mas o mock não é do demonho?

Você já deve ter ouvido que o mock é do mal. Quase qualquer coisa no desenvolvimento de software pode ser usada para o mal, assim como o DRY.

As pessoas acabam chegando numa fase ruim em que não dão atenção aos próprios testes e não respeitam a etapa de refatoração.

Se seu código de mock estiver ficando complicado ou você tem que mockar muita coisa para testar algo, você deve prestar mais atenção a essa sensação ruim e pensar sobre o seu código. Geralmente isso é sinal de que:

  • A coisa que você está testando está tendo que fazer coisas demais

    • Modularize a função para que faça menos coisas

  • Suas dependências estão muito desacopladas

    • Pense e uma forma de consolidar algumas das dependências em um módulo útil

  • Você está se preocupando demais com detalhes de implementação

    • Dê prioridade em testar o comportamento esperado ao invés da implementação

Normalmente, muitos pontos de mock são sinais de abstração ruim no seu código.

As pessoas costumam pensar que essa é uma fraqueza no TDD, mas na verdade é um ponto forte. Testes mal desenvolvidos são resultado de código ruim. Código bem desenvolvido é fácil de ser testado.

Só que mocks e testes ainda estão dificultando minha vida!

Já se deparou com a situação a seguir?

  • Você quer refatorar algo

  • Para isso, você precisa mudar vários testes

  • Você duvida do TDD e cria um post no Medium chamado "Mock é prejudicial"

Isso costuma ser um sinal de que você está testando muito detalhe de implementação. Tente fazer de forma que esteja testando comportamentos úteis, a não ser que a implementação seja tão importante que a falta dela possa fazer o sistema quebrar.

Às vezes é difícil saber qual nível testar exatamente, então aqui vai algumas ideias e regras que tento seguir:

-A definição de refatoração é que o código muda, mas o comportamento permanece o mesmo. Se você decidiu refatorar alguma coisa, na teoria você deve ser capaz de salvar seu código sem que o teste mude. Então, quando estiver escrevendo um teste, pergunte para si: - Estou testando o comportamento que quero ou detalhes de implementação? - Se fosse refatorar esse código, eu teria que fazer muitas mudanças no meu teste?

  • Apesar do Go te deixar testar funções privadas, eu evitaria fazer isso, já que funções privadas costumam ser detalhes de implementação.

  • Se o teste estiver com 3 mocks, esse é um sinal de alerta - hora de repensar no design.

  • Use spies com cuidado. Spies te deixam ver a parte interna do algoritmo que você está escrevendo, o que pode ser bem útil, mas significa que há um acoplamento maior entre o código do teste e a implementação. Certifique-se de que você realmente precisa desses detalhes se você vai colocar um spy neles.

Como sempre, regras no desenvolvimento de software não são realmente regras e podem haver exceções. O artigo do Uncle Bob sobre "Quando mockar" (em inglês) tem alguns pontos excelentes.

Resumo

Mais sobre abordagem TDD

  • Quando se deparar com exemplos menos comuns, divida o problema em "linhas verticais finas". Tente chegar em um ponto onde você tem software em funcionamento com o apoio de testes o mais rápido possível, para evitar cair em armadilhas e se perder.

  • Quando tiver uma parte do software em funcionamento, deve ser mais fácil iterar com etapas pequenas até chegar no software que você precisa.

"Quando usar o desenvolvimento iterativo? Apenas em projetos que você quer obter sucesso."

Martin Fowler.

Mock

  • Sem o mock, partes importantes do seu código não serão testadas. No nosso caso, não seríamos capazes de testar se nosso código pausava em cada impressão, mas existem inúmeros exemplos. Chamar um serviço que pode falhar? Querer testar seu sistema em um estado em particular? É bem difícil testar esses casos sem mock.

  • Sem mocks você pode ter que definir bancos de dados e outras dependências externas só para testar regras de negócio simples. Seus testes provavelmente ficarão mais lentos, resultando em loops de feedback lentos.

  • Ter que se conectar a um banco de dados ou webservice para testar algo vai tornar seus testes frágeis por causa da falta de segurança nesses serviços.

Uma vez que a pessoa aprende a mockar, é bem fácil testar pontos demais de um sistema em termos da forma que ele funciona ao invés do que ele faz. Sempre tenha em mente o valor dos seus testes e qual impacto eles teriam em uma refatoração futura.

Nesse artigo sobre mock, falamos sobre spies, que são um tipo de mock. Aqui estão diferentes tipos de mocks. O Uncle Bob explica os tipos em um artigo bem fácil de ler (em inglês). Nos próximos capítulos, vamos precisar escrever código que depende de outros para obter dados, que é aonde vou mostrar os Stubs em ação.