Como não odiar os seus testes (pt. 2)

Camila Campos
Creditas Tech
Published in
9 min readAug 12, 2019
Escalando no gelo: uma atividade complicada. Photo by Robert Baker on Unsplash

Nós odiamos nossos testes? :O

No meu artigo anterior, dei uma introdução sobre motivos que nos fazem f̶i̶c̶a̶r̶ ̶t̶r̶i̶s̶t̶e̶s̶ ̶c̶o̶m odiar nossos testes e mostrei algumas soluções para um deles, que são os testes muito lentos (se você está lendo esse artigo e ainda não leu o primeiro, corre lá pra ler!). Nessa segunda parte, vou falar sobre um outro motivo: os testes muito complicados.

Um adendo antes de começar: vou mostrar alguns exemplos de código nesse texto (em ruby + RSpec). Eles são bem simples e todos têm explicações sobre o que é importante neles, então não deveria atrapalhar no entendimento do texto.

Testes complicados

Complicado (ADJETIVO) - do latim complicātus
Composto de elementos que entretêm relações numerosas, diversificadas e difíceis de apreender pelo espírito.

(Roubei do Google)

Com relação à testes, são ditos complicados os que são extremamente difíceis e complexos de ler, entender e/ou manter. Muito disso acontece pois falta clareza, tanto na intenção quanto na organização do teste. Outro motivo é porque alguns deles ficam muito “mágicos”, fazendo coisas demais, ou com coisas que não fazemos ideia de onde vem e o que fazem de verdade.

Precisamos lembrar que teste também é código!

Mind Blown. Roubei esse gif daqui.

Quando falamos sobre a falta de clareza em nossos testes, precisamos lembrar que testes são, entre outras coisas, trechos de código que garantem que seu código “de verdade” funciona (ou que está bem escrito). Um detalhe importante sobre essa afirmação é que essa parte do código não é testada (e nem deve ser, né?): isso significa que nossos testes devem ser extremamente simples, objetivos e fáceis de ler.

Imagine, por exemplo, que a gente tenha uma classe de nome PapinhoMaster, que pode cumprimentar as pessoas e que tenha o seguinte teste:

it 'envia uma mensagem' do
pessoa = Pessoa.new(nome: 'Ludmilla')

cumprimento = PapinhoMaster.new(pessoa).cumprimentar

expect(cumprimento).to eq 'Salve, jovem Ludmilla'
end

Apesar de muito simples, descrever esse teste como “envia uma mensagem” não diz muito sobre o que realmente está acontecendo e sendo testado. Eu estou testando que a minha PapinhoMaster está cumprimentando as pessoas (como está codificado no próprio teste, dentro do expect), não mandando uma mensagem qualquer. Pode parecer bobo, mas essa diferença de significado tem um impacto considerável no entendimento do que está rolando nesse teste: além de poder confundir sobre o que realmente estou testando, eu preciso necessariamente ler o teste inteiro para entender do que se trata, ao invés de apenas ler a descrição e estar tranquila com ele.

Portanto, escreva testes com uma boa descrição!

Uma simples troca na descrição do teste, para algo como cumprimenta uma pessoa (no lugar de envia uma mensagem) já deixaria esse teste bem mais claro.

O conteúdo também importa!

Da mesma forma que uma descrição ruim faz com que seu teste seja muito mais difícil de ser entendido, ter coisas não necessárias para o teste dentro dele também confunde quem está lendo esse teste.

Usando o mesmo exemplo da nossa papinho master, com a diferença de que a pessoa que ela cumprimenta agora é criada com nome e sobrenome:

it 'cumprimenta uma pessoa' do
pessoa = Pessoa.new(nome: 'Ludmilla', sobrenome: 'Oliveira')

cumprimento = PapinhoMaster.new(pessoa).cumprimentar

expect(cumprimento).to eq 'Salve, jovem Ludmilla'
end

Olhando para o teste em si, podemos inferir que o sobrenome da Ludmilla não é importante para o cumprimento. Mas e se por algum motivo for (e no meu teste isso não está claro)? Outra dúvida que pode surgir olhando para o teste é com relação à formatação do nome da pessoa ao cumprimentá-la: a minha papinho master muda a primeira letra do nome para maiúsculo, ou ela só usa o mesmo nome que foi usado na criação da pessoa?

Para não ter dúvidas, deixe claro o que não é importante dentro do seu teste! Se possível, crie a pessoa sem o sobrenome e preencha o nome com algo que deixe claro que o formato dele não é importante.

Uma versão mais clara desse teste é:

it 'cumprimenta uma pessoa' do
pessoa = Pessoa.new(nome: 'qualquer nome')

cumprimento = PapinhoMaster.new(pessoa).cumprimentar

expect(cumprimento).to eq 'Salve, jovem qualquer nome'
end

Testes só fazem 3 coisas.

1, 2, 3. Roubei o gif daqui

Existem dois padrões que definem a estrutura dos testes: os “3 As” (AAA): Arrange, Act and Assert e o 4 Phase Test: Setup, Exercise, Verify e Teardown. Em ambos os padrões, são definidos 3 grandes passos do que se é feito durante o teste:

  • Arrange ou Setup: ajeitar tudo que precisam para que eles funcionem;
  • Act ou Exercise: executar a ação que está sendo testada;
  • Assert ou Verify: verificar se aconteceu o que era para acontecer;

Em alguns casos, pode existir um quarto passo para desfazer o que foi feito antes e durante o teste (Teardown do 4 Phase Test). Como nem todo tipo de teste necessita desse passo e muitos frameworks de teste (como o RSpec) já fazem isso automaticamente, não vou focar nele.

Na prática, é importante entender que a separação desses passos, bem como manter uma ordem definida entre eles, ajuda muito na clareza da organização do seu código. Sempre pular uma linha entre esses blocos e mantê-los sempre na mesma ordem (Ajeitar, pular linha, depois Executar, pular linha e, por último, Verificar) faz com que qualquer pessoa olhe para qualquer teste da sua aplicação e saiba exatamente onde está cada coisa.

Para exemplificar, imagine que nossa Papinho Master tenha ganhado poderes de enviar email às pessoas. Para enviar os emails, a classe PapinhoMaster agora usa uma outra classe chamadaMailer. Vamos criar um teste de unidade, que vai usar um test double para simular o comportamento do Mailer (falei sobre tipos de teste e test doubles nesse outro post aqui):

it 'envia email para a pessoa' do
mailer = double('my mailer') # Setup
pessoa = Pessoa.new(email: 'e@mail.com') # Setup
expect(mailer).to receive(:notify).with(pessoa.email) # Verify
PapinhoMaster.new(pessoa, mailer: mailer).enviar_email # Exercise
end

Olhando o teste acima, eu não faço ideia de onde começa e onde termina cada bloco de etapas, sendo que podemos tomar medidas simples como pular linhas e usar ferramentas prontas para que esse teste fique bem mais claro:

it 'envia email para a pessoa' do
# Setup
mailer = spy('my mailer') # Mais sobre spies nesse link
pessoa = Pessoa.new(email: 'e@mail.com')
# Exercise
PapinhoMaster.new(pessoa, mailer: mailer).enviar_email
# Verify
expect(mailer).to have_received(:notify).with(pessoa.email)
end

Mesmo ignorando os comentários do código, é possível ver claramente, sem precisar pensar muito, onde está cada responsabilidade do meu teste.

Teste não nasceu pra ser esperto nem mágico!

Magias do Bob Esponja (Roubei daqui)

Quando os nossos testes estão espertos demais pois fazem coisas demais e estão tão grandes ou complexos que mal dá pra entender o que está rolando, isso geralmente significa que o seu código de “verdade” precisa ser refatorado!

  • Etapas de Setup muito grandes ou complexas significam que seu código real tem muitas dependências, ou muita interação entre elas. Talvez valha a pena melhorar a forma que essas dependências são usadas, ou até juntar algumas delas em uma só.
  • Múltiplas verificações em um mesmo teste geralmente apontam para códigos com muitas responsabilidades. Já pensou que, na verdade, você deveria estar lidando com outros 3 objetos, não só um que faz tudo?
  • Uma etapa de Exercise com muitos passos mostra que a forma como a sua classe está sendo usada é muito complexa. Que tal transformar esses 7 métodos em apenas 1?
  • Qualquer código complexo no teste pode indicar que, na verdade, o que é complexo é seu código de produção. Loops no teste, mocks de métodos privados da própria classe, entre outros, são exemplos de alguns desses problemas.

Vale dizer que nada disso acima é verdade para todos os casos. Saiba analisar cada um e ver quando existe realmente um problema para só então aplicar a refatoração.

D.R.Y. não foi pensado para testes!

Terra seca. Photo by Brad Helmink on Unsplash

Um conceito da programação que nos é ensinado desde cedo é o D.R.Y. (Don’t repeat yourself. Em português, não repita a si mesmo), cuja grande ideia é não repetir coisas desnecessárias no nosso código (e parece uma boa ideia mesmo. Inclusive, façam isso!).

Agora imagine que a nossa Papinho Master, além de cumprimentar uma pessoa e enviar emails, começa a cumprimentar uma pessoa de uma forma mais formal.

it 'cumprimenta uma pessoa de maneira super chique e formal' do
pessoa = Pessoa.new(nome: 'qualquer nome')

cumprimento = PapinhoMaster.new(pessoa).cumprimentar_formalmente

expect(cumprimento).to eq(
'Olá, qualquer nome! É um prazer inenarrável te ter por aqui.'
)
end

Pra quem tem memória boa, pode perceber que as únicas coisas que mudaram entre esse teste e o primeiro teste que fizemos (de cumprimentar) são o nome do método que chamamos e a resposta do cumprimento chamado. Como boas pessoas desenvolvedoras, nós sempre fazemos de tudo para usar os princípios de programação que aprendemos, como o DRY, e ver esse monte de coisa repetida é o que basta pra que a gente dê um jeito de limpar esse código.

Uma versão super DRY dos 3 testes da nossa Papinho Master seria:

let(:mailer) { spy('Meu mailer maroto') }
let(:pessoa) {
Pessoa.new(nome: 'um nome', email: 'e@mail.com', mailer: mailer)
}
let(:papinho) { PapinhoMaster.new(pessoa) }

it 'cumprimenta uma pessoa' do
expect(papinho.cumprimentar).to eq "Salve, jovem #{pessoa.nome}"
end
it 'cumprimenta uma pessoa de maneira super chique e formal' do
expect(papinho.cumprimentar).to eq(
"Olá, #{pessoa.nome}! É um prazer inenarrável te ter por aqui."
)
end
it 'envia email para a pessoa' do
papinho
.enviar_email
expect(mailer).to have_received(:notify).with(pessoa.email)
end

Cada teste meu está super conciso e sem repetições, mas se eu foco em um deles apenas, como o de enviar o email, eu perco todo o entendimento do que está realmente rolando nesse teste. Surgem perguntas como “quem é papinho mesmo?”, “De onde vem esse mailer?”, “De onde vem essa pessoa?” ou “Por que o email dessa pessoa em específico está sendo usado no mailer?”.

Isso é um problema conhecido como Mistery Guest (em português, convidado misterioso): tem um “convidado” — intruso — no meio do nosso teste que ninguém faz ideia de onde vem.

Como só tem 3 testes nesse exemplo, pode ser que essas dúvidas sejam rapidamente sanadas, mas imagina só se tivéssemos mais outros 10 casos de teste para a nossa papinho master! Seria impossível entender um teste sem subir e descer a página algumas vezes.

Por isso, para a escrita de testes, é melhor que você tenha repetições ao invés de ter algo muito DRY, mas que seja quase impossível de ser entendido.

E é pra deixar os testes gigantes mesmo?

Uma dica para evitar que seus testes fiquem muito grandes e repetitivos, é utilizar métodos auxiliares que simplifiquem as coisas, mas ainda deixem a intenção de cada parte do código bem clara. Pra fechar os exemplos dessa rodada, vai um coloridinho em forma de gist:

Apesar de maiores que os testes anteriores, nesses eu consigo olhar para qualquer um e entender exatamente tudo que está acontecendo. Como teste é um código que precisa ser muito simples, a repetição e o tamanho aparentemente maior são características mais interessantes de se ter e aplicar do que evitar (como geralmente é feito no código de produção).

Ainda não acabou :O

A segunda parte já ficou gigante, então vou deixar para falar de testes desnecessários na terceira haha

Já falamos sobre testes muito lentos ou muito complicados nas partes 1 e 2, respectivamente. Para entender melhor sobre testes desnecessários e ver alguns exemplos mais claros de code smells (em ruby), aguarde os próximos capítulos ❤ Me acompanhe no twitter que postarei novidades lá!

Mal posso esperar (2)

Esse artigo é o segundo de uma série de posts que é um compilado do que foi falado em palestras dadas na RubyConfBR 2017, TDC São Paulo e Florianópolis 2018 e The Conf 2018.

Tem interesse em trabalhar conosco? Nós estamos sempre procurando por pessoas apaixonadas por tecnologia para fazer parte da nossa tripulação! Você pode conferir nossas vagas aqui.

--

--

Camila Campos
Creditas Tech

Uma dev doida, apaixonada por testes e qualidade de código, que trabalha na SumUp e que quer incluir mais mulheres na computação através do Rails Girls SP.