Mocks
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.
Vamos resolver isso escrevendo uma função chamada Contagem
que vamos colocar dentro de um programa main
e se parecer com algo assim:
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.
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 oos.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
:
Tente novamente:
argumentos demais na chamada para Contagem
O compilador está te dizendo como a assinatura da função deve ser, então é só atualizá-la.
contagem_test.go:17: resultado '', esperado '3'
Perfeito!
Escreva código o suficiente para fazer o teste passar
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.
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.
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.
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
Escreva código o suficiente para fazer o teste passar
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.
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.
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.
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.
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.
Execute o teste
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
:
Se tentar novamente, nossa main
não vai mais compilar pelo mesmo motivo:
Vamos criar um sleeper de verdade que implementa a interface que precisamos:
Podemos usá-lo na nossa aplicação real, como:
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.
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:
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.
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.
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.
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.
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.
Definindo nosso spy, podemos criar um novo teste para o sleeper configurável.
Não há nada de novo nesse teste e seu funcionamento é bem semelhante aos testes com mock anteriores.
Execute o teste
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
Com nossa nova função Pausa
implementada, ainda há um teste falhando.
Escreva código o suficiente para fazer o teste passar
Tudo o que precisamos fazer agora é implementar a função Pausa
para o SleeperConfiguravel
.
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.
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.
Last updated