Pensando em testes - Parte 1

Uma nova série de posts - Pensando em testes: como evitar que seus testes automatizados se tornem um problema.

Pensando em testes - Parte 1
Foto de Tara Winstead: https://www.pexels.com/pt-br/foto/caixas-baus-arcas-embalagens-8850706/ - alterada por mim

Sabe o que pode ser pior do que não ter testes automatizados? Ter o caos disfarçado de testes automatizados. De nada adianta dizermos que temos uma imensidão de testes se:

  • Você não sabe quais testes são de fato úteis.
  • A funcionalidade validada não está clara no código fonte destes testes.
  • Se você só tem testes unitários que não validam a sua aplicação de fato em execução (sim, testes integrados são fundamentais).
  • Leva tanto tempo para executar seus testes que sua equipe cria resistência (sejamos honestos, dá preguiça) de executar a suite de testes.
  • Se o código dos testes é de péssima qualidade (who watches the watchers?).
  • Pegando o ponto anterior: se o código é tão ruim que você não consegue otimizar a execução dos testes e dar manutenção nos mesmos.
  • Você não sabe qual ou quais testes executar para validar uma mudança (sério que sempre precisa executar TUDO?)

Se você está na situação acima (que é bem normal) na prática seus testes não estão agregando valor: estão tornando sua vida mais difícil e talvez piorando a qualidade do seu sistema ou ocultando problemas.

Este é o primeiro post de uma série sobre a qual o assunto são testes automatizados. Começo pelo problema mais difícil: como pensamos os testes.

💡
Antes de prosseguir, alguns avisos.

Os ganhos das práticas e ideias que vou descrever a seguir observei na equipe da itexto e também em nossos clientes. Talvez não seja o seu caso. Leia tudo com visão bem crítica.

Testes aqui estou focando no backend, mas também pode se aplicar a testes funcionais (Cypress, por exemplo) ou mesmo de frontend.

Comece pelos valores

Foto de Vicky Tran: https://www.pexels.com/pt-br/foto/pessoa-em-pe-na-flecha-1745766/

O mais importante é expor o valor de testes automatizados para sua equipe. E tem de ter muito cuidado pois é facílimo transformar a escrita de testes em um antagonista do seu time.

💡
Um dos pilares da itexto se chama valor.

Valor é a justificativa por trás de uma escolha. Um dos maiores riscos que vemos em projetos é a subjetividade.

Esta justificativa (ou justificativas) não pode ser subjetiva. Frases como "por que sim", "por que é bonito", "por que fulano disse que é bom" não são válidas para nós.

Tem de ser baseada em fatos e restrições do contexto em que você irá atuar, preferencialmente fruto de um diálogo entre sua equipe e o cliente.

Por muito tempo achava linda uma frase do Robert C. Martin: "um programador que não escreve testes é como um cirurgião que não lava as mãos antes de operar". Parece linda certo, mas não é:

  • A comparação do cirurgião com o programador é descabida (você não tem "releases" em cirurgias).
  • Você está jogando um preceito moral e não uma justificativa para seu time. As pessoas podem até negar mas - na minha experiência - morrem de preguiça de todo moralista e vão ver os testes como obrigação, não como valor.

É fundamental você expor ao time os valores de testes automatizados:

  • Como ponto de parada - se você escrever os testes primeiro (vou falar sobre isto a seguir) vai saber quando a funcionalidade está pronta (quando os testes passam).
  • Como ferramenta de design - é o momento em que você pode verificar se a forma das suas classes e a assinatura dos seus métodos realmente faz sentido.
  • Como validação do seu trabalho - te permite mostrar ao cliente que o que aquele seu código realmente faz o que você recebeu para fazer.
  • Documenta a funcionalidade - mostra ao time como aquela funcionalidade opera e o que esperar da mesma (e NÃO, só código não é documentação suficiente).
  • Te oferece segurança - você vai saber quando seu código quebrou alguma funcionalidade. E é um sonho se seu objetivo é refatorar ou melhorar o desempenho.
  • Facilita a sua vida - não é necessário verificar se está funcionando com aquele ciclo de iniciar a aplicação, interagir manualmente com ela e nisto ver o resultado também manualmente na saída ou bancos de dados usados pelo seu código.

E estes são apenas alguns dos valores que me vieram à mente escrevendo este texto. E sabem qual é aquele que normalmente - na minha experiência - mais atraí as pessoas para escrever testes? Facilitar a vida.

Na minha experiência pessoal foi justamente ter notado que eu ganhava HORRORES em produtividade ao escrever os testes que me convenceu a adota-los no meu dia a dia. E sem sombra de dúvidas foi o maior salto de produtividade e qualidade de vida que experimentei na minha carreira.

💡
É engraçado: lembrando hoje noto que minha preocupação inicial não era garantir que o código estava correto, mas sim entregar mais rápido.

Soa estranho (talvez até errado), mas a qualidade para o Kico de décadas atrás veio como consequência deste primeiro valor (produtividade).

Como líder técnico/arquiteto este é seu objetivo mais importante e difícil de ser atingido: convencer a equipe (e cliente) usando valores de que os testes valem à pena.

💡
Como freelancer ou indivíduo também: a segurança que você ganha com uma ferramenta que valida para seu cliente que o que foi feito está correto não tem preço.

Termos importam - sai "teste", entra "especificação (spec)"

Foto de Ravi Kant: https://www.pexels.com/pt-br/foto/foto-de-close-up-das-paginas-do-livro-2877338/

Aprender TDD (Test Driven Development) foi meu momento eureka : a produtividade aumentou HORRORES por que ao invés de executar todo o fluxo da aplicação manualmente, agora eu podia escrever um teste unitário bem simples que fazia aquilo pra mim de forma automatizada.

Se o código que eu ia validar precisasse de uma dependência, podia ir criando mocks que simulavam todos os comportamentos externos que eram necessários. Por um bom tempo foi maravilhoso, mas aí veio o preço: testes em excesso.

💡
Ah... as licitações que exigiam "cobertura de testes".

Na minha opinião foram péssimas pois motivaram muita gente a escrever testes absolutamente inúteis (de getters e setters em Java por exemplo) só para obter 100% de cobertura...

Foi quando conheci o BDD (Behaviour Driven Development): e se eu trocasse a palavra "teste" por "spec"? É uma mudança sutil, mas foi o segundo salto de produtividade na minha carreira.

É muito sutil mas uma troca muito poderosa: enquanto em testes unitários valido a execução de uma função isolada, que tal escrever não testes, mas especificações funcionais que validem o comportamento da funcionalidade sob o ponto de vista de quem realmente importa: o usuário final?

Foi MARAVILHOSO por que pela primeira vez se tornou claro um problema que não conseguia apontar: testes que não tinham valor algum, que haviam sido escritos só como ferramentas de desenvolvimento criadas pelo autor do código original.

Agora eu começava do topo: escrevendo código que executava diretamente os endpoints que eram acessados pelos usuários finais.

E daí, a partir do topo, caso necessário (sempre é) ia escrevendo testes unitários de apoio para o que era realmente importante para a execução correta do código. E sabe o que era mais incrível? Eu ainda tinha uma alta cobertura de código na execução das minhas specs!

E aqui entraram os ganhos:

  • As specs documentavam a funcionalidade do sistema do ponto de vista do usuário final.
  • O número de testes inúteis é significativamente reduzido.

E você não precisa adotar ferramentas como Cucumber ou Spock para tal: usando seu framework de testes unitários padrão já dá pra começar. Olha um exemplo escrito em Go (código real):

/*
*
	POST /api/v1/auth

	Se um usuário inexistente tenta se autenticar na plataforma,
    o código HTTP 401 deve ser retornado informando que a 
    autenticação foi mal sucedida
*/
func (suite *AcessoTestSuite) TestAcessoNegadoUsuarioInexistente() {

	request := utils.CredentialsRequest{
		Username: "inexistente@inexistente.com",
		Password: "SuperInexistenteMesmo!",
	}
	w := httptest.NewRecorder()
	requestJson, _ := json.Marshal(request)

	requestJsonString := string(requestJson)
	req, _ := http.NewRequest("POST", "/api/v1/auth", strings.NewReader(requestJsonString))
	req.Header.Set("Content-Type", "application/json")
	suite.router.ServeHTTP(w, req)

	suite.Equal(http.StatusUnauthorized, w.Code)
}

Sabe aquele papo de que "comentários devem ser evitados"? Balela. Observe como o comentário descreve o comportamento esperado (ele pode ser lido por alguém que não saiba programar). Além disto, mostramos também qual o endpoint que está sendo exercitado.

💡
Robert C. Martin é um destes autores que recomendo muito a leitura crítica para que você discorde do que está sendo dito. Aprendi demais discordando dele.

O que ele escreve sobre comentários é uma das maiores desserviços já causados à nossa área. Escrevi sobre isto neste link.

Notou que legal? É o código como documentação legível para a sua equipe de negócios e, mais importante: seu CLIENTE (claro que ele não vai ler seus testes, mas você vai antes de lhe reponder uma dúvida).

Fica mais fácil saber pelo código como algo funciona.

💡
Por que no restante deste texto e no próximo uso o termo "teste" e não "spec"?

Por duas razões: vício de linguagem e por que toda spec na realidade é um teste, só que níveis de abstração acima.

E você não vai substituir tudo por specs, os testes ainda são necessários pra validar detalhes, por exemplo, ou mesmo como suporte de validação.

Arquitetura nos testes é fundamental

Este é o assunto do próximo post, mas já adianto aqui. Quando as pessoas entendem o valor dos testes automatizados é comum que escrevam muitos testes por pura empolgação.

E nisto acabam acidentalmente se esquecendo de todas as boas práticas que aplicaram no código que estão validando. Código não duplicado, mas MULTIPLICADO, como um gremlin que acidentalmente se molhou.

Defendo que o mesmo cuidado aplicado na arquitetura da aplicação também se aplique aos testes: do modo como o código de testes é organizado, quais padrões são adotados, restrições adotadas, gestão de dados, versionamento e tudo.

Vou até além: acredito que a documentação do código de teste seja talvez mais importante que a do próprio código testado (não estou dizendo para não documentar o código validado). No mínimo ela te provê uma documentação que te ajuda a entender sua plataforma.

Testes integrados não são ruins

É muito comum encontrar bases de código que só tenham testes unitários. A justificativa normalmente é a de que "testes integrados são lentos" ou que "testes integrados são mais difíceis de configurar".

Os dois pontos se resolvem pensando na arquitetura do código de teste. O grande problema em se ter apenas testes unitários é que você não vai ter validado o que REALMENTE importa: seu código no ambiente de produção.

É muito fácil criar uma malha de testes solipsista: validam cada detalhe do código, mas na hora em que o sistema é implantado, torna-se necessária uma segunda leva de validações (normalmente manuais ou funcionais usando algo como Cypress) que poderia ser evitada se houvessem testes integrados.

💡
Sabe o clássico "na minha máquina funciona"?

Existe uma variante terrível: "nos testes funciona"!

Pense criticamente: se é um código que irá manipular registros no banco de dados, por exemplo, REALMENTE agrega valor substituir totalmente o banco de dados por um mock? (banco de dados não é um detalhe)

Não estou dizendo para você substituir todos os seus testes unitários por integrados (já fiz isto várias vezes, pra sempre no futuro substituir vários testes integrados por uma leva de testes unitários), mas sim pra salientar que precisamos de testes integrados também.

Tem de haver um equilíbrio entre testes unitários e integrados: um complementa o outro.

Testes pessoais e da equipe

Antes de terminar, um ponto importante que deve ser levado em consideração: saber diferenciar entre testes pessoais e da equipe.

Lembra quando disse que um dos valores mais adorados é tornar a vida de quem programa mais fácil? Então: muitos testes na realidade não tem valor algum além de simplesmente executar o código. Normalmente estes "testes" são apenas uma ferramenta de produtividade do autor original.

O teste ou validação real é aquele que vai além de apenas executar algo para ver se um erro ocorre: é aquele que descreve o comportamento esperado. Todos na equipe devem ter bem claro este princípio.

Se no repositório não existirem apenas os testes que pertençam à equipe, já sabe: no futuro dificilmente você conseguirá distinguir os testes úteis dos inúteis.

Resumindo: só envie para o repositório testes que agreguem valor real.

Continuando

Neste post tratei daquele que é o problema que considero mais difícil quando o assunto são testes automatizados: nossa postura e relacionamento com estes.

No próximo vou mostrar algumas práticas que adoto em nossas suites de specs/testes que tornam nossa vida muito mais fácil.

Mantido por itexto Consultoria