6 dicas para entender mais sobre desenvolvimento de testes | Venturus
Texto original pode ser encontrado neste link.
Teste é algo extremamente importante em todos os projetos de software. O uso de testes deixa mais fácil alterar projetos ou incluir novas funcionalidades, dá mais confiança às pessoas desenvolvedoras e ajuda na descoberta de falhas o mais cedo possível.
No entanto, pode não ser tão simples encontrar projetos e times que realmente foquem no desenvolvimento de testes, seja por falta de experiência ou por não dar a importância necessária. O meu sentimento é que teste de software é um tema subestimado. Quantos projetos que você já participou e ouviu a frase "vamos desenvolver a funcionalidade e depois faremos os testes quando tivermos tempo"? Isso mostra o quanto alguns projetos ainda não conseguem ver o valor de testes.
Nesse artigo, trago algumas dicas e experiências e procuro desmitificar alguns conceitos sobre testes, para incentivar uma reflexão sobre como eles podem mudar nossas experiências de trabalho.
Test-Driven Development é excelente, mas não é a única solução
É muito difícil falarmos sobre testes sem mencionarmos Test-Driven Development. O conceito de fazer um teste que falhe, fazer o teste passar ou refatorar o teste é uma técnica muito difundida entre os entusiastas de teste de software. E não é à toa: essa técnica traz muita confiança, devido ao feedback rápido e contínuo que temos durante todo o processo de desenvolvimento.
Depois de começar a desenvolver projetos usando TDD, eu sinto muita dificuldade de não utilizar essa técnica em alguns momentos. A sensação de ir passo a passo, pensar cuidadosamente nos casos de exceção e receber uma resposta rápida, que indica se o código que você acabou de fazer quebra algo já implementado, é uma sensação incrível.
Entretanto, TDD não é a única solução para implementarmos testes nos nossos projetos. Não se sinta pressionado a aplicar essa técnica no seu projeto porque ela é a "maneira correta" de se fazer testes. O ponto mais importante é que o seu projeto tenha testes significativos, que dêem confiança e controle sobre a execução do software. Não se preocupe tanto se os testes são feitos antes, durante ou ao entregar a nova funcionalidade.
Ainda na discussão sobre Test-Driven Development, recomendo uma série de vídeos realizado por Kent Beck (criador do TDD, Junit, Extreme Programming etc.), Martin Fowler (um dos maiores autores de literatura de software) e David Heinemeier Hansson (criador do Ruby On Rails). Durante essas discussões, intituladas "Is TDD Dead?", são questionados diversos pontos e recomendo o conteúdo até mesmo a pessoas desenvolvedoras que já tem uma opinião formada sobre o assunto.
As definições não são tão claras
Pessoas desenvolvedoras adoram dicotomias: se algo é verdadeiro, então, não pode ser falso; se algo é 1, não pode ser 0. Infelizmente, quando falamos sobre testes, alguns conceitos não se encaixam tão bem nessas caixinhas. Logo de cara, o termo "Teste Unitário" pode variar, dependendo da fonte da sua pesquisa. Quando questionado sobre uma definição, Kent Beck respondeu que, durante a primeira manhã de seu curso, são abordadas 24 definições de teste unitário.
Apesar de termos várias definições, de acordo com Martin Fowler, os testes unitários possuem três elementos distintos se comparados a outros tipos de teste. Assim, testes unitários são:
De baixo nível, focados em pequenas partes de um sistema de software;
Escritos por desenvolvedores, usando suas próprias ferramentas;
Executados mais rápido do que outros tipos de testes.
No entanto, até mesmo o ponto da velocidade do teste é um ponto comum de discordância entre as pessoas desenvolvedoras. Alguns presam mais pela autenticidade dos testes, ao custo de sacrificar um pouco a velocidade; enquanto outros presam por rodar testes em milésimos de segundos.
As linhas que separam os diferentes tipos de testes dentro da pirâmide também não ficam de fora desses conflitos. Por exemplo, ao adicionarmos uma operação de banco de dados nos nossos testes, mesmo que ele seja executado em poucos milissegundos, ele pode ser considerado unitário? Quando estamos usando TDD, os testes são considerados de caixa-preta ou caixa-branca? Ao pensar continuamente em testes, você é uma pessoa testadora ou uma pessoa desenvolvedora? Para mostrar que nem tudo se encaixa em dicotomias, Kent Beck tem um artigo muito esclarecedor.
A verdade é que existe muita discussão sobre as terminologias e sobre onde começa um tipo de teste e acaba outro. Minha sugestão é focar em criar testes significativos, que deixem você confiante ao alterar seu projeto, não se preocupando tanto em qual pedaço da pirâmide eles estão. Não se importe tanto se o teste é executado em 1 minuto ou 1 segundo, desde que ele agregue valor.
Evite testar implementações, teste comportamentos
Um problema muito comum para quem está iniciando no mundo de testes é desenvolver um teste para cada nova classe ou função adicionadas ao projeto. Isso se torna um problema quando o código vai ser refatorado e percebemos que os testes não podem mais ser executados corretamente pois eles estão muito acoplados ao código de produção. Ao invés disso, uma boa prática é pensarmos em testar os comportamentos da aplicação. O que o nosso projeto faz é mais importante de que como ele faz.
O Ian Cooper tem um excelente talk falando sobre interpretações erradas do Test-Driven Development. Apesar de ter um foco mais em TDD, é um vídeo muito esclarecedor e aborda conceitos que podem ser usados em diferentes técnicas de testes. Seguem alguns dos pontos levantados por ele que melhoram muito a minha visão sobre testes:
Não escreva teste para detalhes de implementação
Os detalhes de implementação de um projeto mudam demais ao longo do tempo, seja porque uma classe ou função foi removida, renomeada ou refatorada em diferentes arquivos. Quando criamos testes com a premissa de "verifique que a função x foi chamada", é bem provável que, se esse código for atualizado, os testes não irão nem compilar mais.
Nem toda nova classe ou função deve gerar um novo teste
Não é porque criamos uma nova classe ou função no nosso projeto que devemos criar um teste para cobrir esse código. Alguns trechos de códigos são cobertos indiretamente por testes focados em comportamento. Isso é especialmente verdade quando a nova classe/função é interna ou privada (visível apenas em um determinado escopo, sem acesso por outro módulo ou cliente).
Criar novos testes é ótimo para aumentar a confiabilidade do código, mas criar testes desnecessários deixa o código rígido e de difícil manutenção. Sempre se questione antes de adicionar um novo teste e não tenha receio de apagar os que não fazem mais sentido ou que estão engessando o seu ciclo de desenvolvimento.
Um novo comportamento deve gerar novos testes
Quando o seu software recebe um novo comportamento, isso, sim, deve gerar um novo teste, que cubra esse novo requisito. Tente manter o foco no que a sua aplicação deve executar. Por exemplo, se o projeto em que trabalha é de um software de agenda, requisitos como "adicionar um evento", "adicionar um contato", "convidar um contato para um evento" devem ser os requisitos a serem testados. Saber se o projeto precisa se comunicar com 1 ou com 5 classes para adicionar um contato não é responsabilidade do teste.
Não teste internos
Tudo que for interno ao seu software, seja private, protected ou internal, só diz respeito a detalhes da implementação. Esses tipos de código tem uma chance muito grande de ser refatorado e, como já mencionamos anteriormente, não devem ser testados diretamente. Sim, eles estarão cobertos por testes, mas de uma forma indireta.
Uma boa maneira de fazer essa cobertura é criar uma camada pública (API) que seja testável. Com essa camada, é possível testar as entradas e saídas do seu software, validando os requisitos, mas sem testar a camada interna. Uma outra recomendação é: nunca deixe o encapsulamento mais permissivo para testar algo. Deixar uma função pública para facilitar o teste está evidenciando que você está focando em detalhes, não no comportamento.
Tome cuidado com o uso de Mock
O uso de Mocks facilita muito a execução de alguns testes em que não queremos usar uma implementação concreta ou a simulação de um comportamento que pode demorar alguns segundos, como acesso ao banco de dados. Mas essa ferramenta também tem um custo: o código de testes precisa saber um pouco mais sobre alguns detalhes da implementação do System Under Test (SUT).
Geralmente, a configuração de um mock consiste em "quando a função x for chamada, faça ela retornar y". Com isso, voltamos ao ponto que o teste não deveria saber detalhes da implementação e, caso esses detalhes mudem, como a função ser renomeada, os testes não irão mais compilar.
Quando temos testes que não compilam mais, toda a segurança que queremos ter no momento de refatorar um código vai embora. O teste está quebrando porque eu alterei o comportamento durante a refatoração ou porque quando fiz ele compilar de novo, eu quebrei o teste? E lá se foi a confiança.
Durante um vídeo discutindo sobre testes, Kent Beck compartilhou a sua experiência sobre o assunto:
"Eu faço mock de absolutamente tudo? Minha prática pessoal é: eu faço mock de quase nada. Se eu não consigo descobrir como testar de forma eficiente com o código real, eu encontro outra maneira de criar um ciclo de feedback para mim."
E essa frase me leva ao próximo ponto:
Mock não é a única forma de simular comportamento
Apesar de Mock ser a maneira mais conhecida de simular comportamentos nos testes, ele não é a única forma. Em mais um excelente artigo chamado "Mocks Aren't Stubs", Martin Fowler menciona as definições de cada um dos 5 dublês de testes que existem em desenvolvimento de software, criada por Gerard Meszaros.
Esses outros dublês de testes dão mais flexibilidade e controle, ao mesmo tempo que escondem um pouco mais os detalhes de implementação do código de teste. Eu também concordo que podemos simplificar esses 5 dublês em apenas 2 dublês com base no comportamento que você deseja. Na minha experiência pessoal, eu tento usar a implementação real sempre que possível. Se não for possível, eu prefiro criar um dublê do tipo Fake ao invés de implementar Mocks.
Bons testes levam a um bom design (e vice-versa)
Ao desenvolver testes usando TDD, eu me questionei muito mais sobre o design e arquitetura do software. Perguntas simples como "mas como eu faria para testar isso?" ou "os testes estão ficando muito complexos, será que essa classe está fazendo coisas demais?" ajudam muito a criar um código melhor.
Conceitos como Clean Architecture, Princípios SOLID e Pair Programming encaixam muito bem com todo esse ambiente pensando em testes. É muito difícil testar uma base de código que não possui um bom design: é complicado substituir as implementações concretas por dublês, a configuração para testes simples fica imensa e provavelmente teremos que confiar mais em testes manuais e de UI do que Testes Unitários.
Desenvolver e pensar em testes é uma responsabilidade de todos os integrantes do projeto, não algo para ser simplesmente jogado no colo das pessoas testadoras. Uma base de código bem testada traz paz de espírito para todos do time.
Desenvolver testes irá deixar o seu projeto mais confiável, fácil de ser refatorado e introduzir novas funcionalidades. Isso também fará você crescer muito como pessoa desenvolvedora, fornecendo mais ferramentas e técnicas para que você crie códigos cada vez melhores.
A ideia desse artigo foi compartilhar algumas técnicas e insights que tive durante essa minha jornada para aprender mais testes, para que você não tropece nas mesmas pedras que tive dificuldade. Além disso, esse artigo é para aquecer a discussão de toda a importância que esse assunto tem. Eu acredito que adicionar testes significativos nosso seu projeto, mesmo que se você tem certeza de que técnicas utilizar, é melhor do que não ter teste nenhum.