Princípios do Test Driven Development (TDD) com Python

Princípios do Test Driven Development (TDD) com Python

É comum quando começamos um novo sistema iniciarmos projetando funcionalidades básicas e testando manualmente para garantir que cada pedaço de código esteja funcionando conforme o esperado.

No entanto, conforme vamos progredindo na carreira de desenvolvedor, vamos nos envolvendo com projetos cada vez mais complexos, e então, se torna necessário uma maneira mais eficaz para gerenciar o nosso código.

Uma das chamadas metodologias ágeis, que pode ser utilizada para essa tarefa é o Test Driven Development (TDD), que detalharemos neste artigo com Python.

O que é o TDD?

Traduzindo, o TDD significa desenvolvimento orientado por testes, essa metodologia consiste em escrever o código de teste antes de propriamente ter um código funcional. Isso possibilita que o desenvolvedor consiga um ciclo de desenvolvimento muito mais ágil, escrevendo primeiro um código que vai falhar e retornar um erro, depois um código para passar no teste e, por fim, o código é refatorado. Esse ciclo é conhecido como red, green, refactor.

Dentre as vantagens de utilizar essa metodologia estão: saber com antecedência o que esperar do código, definindo os tipos de dados que as funções vão receber e retornar; reduzir o número de bugs ao escrever testes para cobrir todas as funcionalidades do código e mais facilidade de manutenção, dado que se uma alteração fizer com que o sistema pare de funcionar corretamente, os testes vão apontar o local do problema.


O que deve ser testado?

Primeiro, vale ressaltar que existem diferentes tipos de testes, sendo eles: os unitários que garantem a funcionalidade de pequenas unidades de código, como uma função; os de performance que determinam a fluidez do código; segurança que vai garantir que o código não possa ser explorado por um usuário mal intencionado, dentre outros.

O que deve ser testado principalmente são as regras de negócio, ou seja, o código de funções e classes que vão realizar algum cálculo, processar dados ou tomar decisões. Isso inclui verificar os inputs que são passados para saber se os tipos de dados estão corretos, no formato adequado e no alcance (range) esperado.

Verificar também se quando passado um dado errado, o código vai retornar o erro adequado e verificar o código que interage com dependências externas, como bancos de dados, API's ou bibliotecas de terceiros.

Como começar a desenvolver um sistema?

O primeiro passo para criar um novo sistema é compreender o problema que você está tentando resolver, sabendo que existem diferentes tipos de problemas e formas de resolvê-los.

Você pode ter um bug em um programa já existente onde você tem que olhar o erro que está sendo retornado e corrigi-lo ou um problema mais abstrato, como por exemplo a experiência do usuário com algum produto ou serviço.

Comece definindo o contexto, o que precisa ser feito? Depois desenvolva uma visão clara do porquê um novo sistema deve ser projetado, por que isso é um problema? Quais os benefícios que resolver esse problema vai trazer?

Feito este exercício mental, o próximo passo é pesquisar para saber se outras pessoas já tiveram esse mesmo problema e como elas resolveram. Caso não encontre uma solução de imediato, pesquise por caminhos que poderiam ser uma maneira de resolver essa questão.

Depois disso, é hora de começar a projetar um algoritmo, que em matemática e ciência da computação, é uma sequência de ações lógicas que visam obter a solução para um determinado problema. Neste primeiro momento, não é necessário escrever um código funcional, você pode começar criando um fluxograma ou um pseudocódigo como comentário no seu editor de código, usando um bloco de notas, um aplicativo específico ou até mesmo com papel e caneta.

Entendendo o ciclo red, green, refactor

Escrevendo o teste que falha (red)

Com um algoritmo básico em mãos, podemos começar a escrever os testes. A primeira etapa do ciclo é criar um teste que vai falhar, daí o nome red que é a cor que está associada com o teste fracassando. Vou utilizar a biblioteca nativa do Python unittest e criar uma calculadora básica como exemplo.

Primeiro começaremos criando um arquivo de testes chamado test_calculator.py e um arquivo para o código da calculadora, que chamaremos de calculator.py, dentro do arquivo de testes, importaremos o módulo unittest e todo o conteúdo do arquivo do código da calculadora:

Em seguida, criaremos uma classe para testar a nossa calculadora, ela vai herdar da classe TestCase do módulo unittest e dentro dela vamos definir os testes para somar, subtrair, multiplicar e dividir.

No fim, chamaremos a função main do unittest para rodar os testes:


Note que todas as funções começam com o nome test, seguido de um underline e sua descrição, isso é importante pois é assim que o unittest interpreta que aquela função dentro da classe TestCase é um teste e a executa, se não tiver o nome test no início, o teste não será rodado.

A forma como fazemos as validações nos testes é chamando a função assert, neste exemplo utilizamos o assertEqual que compara se dois valores são iguais, se eles forem, retorna verdadeiro e os testes passam, caso contrário, irá retornar falso, o que significa que o mesmo não passou. Existem várias outras funções dessas como o assertTrue, que verifica se um valor é verdadeiro; o assertFalse que faz o contrário, verificando se um valor é falso; o assertNotEqual que verifica se dois valores são diferentes, dentre outros.

Outro ponto relevante que você notará é que dentro de cada função de teste foi criada propositalmente uma nova instância da classe Calculator, isso para mostrar que, se você tiver um caso em que são necessárias várias funções de teste criarem e utilizarem um mesmo objeto, você pode incluir na função setUp, então dentro de cada função você pode acessar ele através do self:

O setUp é uma função especial da TestCase que nos permite definir atributos para a classe que poderão ser usados pelos métodos. Assim como ela, existe o tearDown, que é uma função que será chamada quando o TestCase acabar. Um exemplo de uso prático para essas funcionalidades é quando você estiver testando um banco de dados, no setUp você pode abrir uma conexão e no tearDown você fecha a conexão com o banco.

Ao rodar o comando python test_calculator.py no terminal, veremos que será retornado quatro erros conforme o esperado, visto que não implementamos nenhum código funcional ainda.

Escrevendo o código para passar no teste (green)

A próxima etapa é de fato começar a implementar o código funcional, aqui é importante escrever apenas o mínimo de código para passar nos testes. O código para fazer os testes escritos passarem neste exemplo seria este:


Ao escrever este código no arquivo calculator.py e rodar novamente o comando no terminal, os testes passarão, pois agora temos implementado a classe que define a calculadora e as operações básicas.

Refatorando o código (refactor)

A última etapa desse ciclo é a refatoração do código, neste ponto temos algumas questões a considerar, como a legibilidade, o desacoplamento e a performance.

Quanto a legibilidade, no caso do Python que é uma linguagem dinamicamente tipada, ou seja, que o tipo das variáveis é determinado quando o programa é executado, podemos adicionar dicas de tipos (type hinting), uma funcionalidade que foi criada nas novas versões do Python que nos permite saber o que uma função vai receber como parâmetro ou retornar:


Em outras linguagens de programação que não possuem tipagem dinâmica isso não é necessário, pois a indicação do tipo já é algo obrigatório. Vale ressaltar ainda que essa prática não muda o comportamento da linguagem, como o próprio nome sugere, é apenas uma dica de tipo, para o desenvolvedor saber o comportamento padrão de uma função, mas dizer que um parâmetro é do tipo int, não nos impede de passar uma string para ele no Python.

Ainda falando sobre a legibilidade, você também pode adicionar comentários e mudar o nome das variáveis nesse ponto, se achar necessário.

Já a prática de desacoplar o seu código serve para melhorar no quesito da manutenção e reusabilidade. Criar funções que façam muitas coisas pode dificultar na hora que você precisar dar manutenção naquele código, adicionando uma nova funcionalidade ou corrigindo um bug e também no quesito da reusabilidade, pois ter muitas tarefas implica dizer que todo um pedaço de código só será utilizado em um lugar específico, quando na prática, ocorre que essa função poderia ser decomposta em várias outras que poderiam ser reutilizadas em outras partes do código.

Outro ponto a se considerar quando estamos refatorando, é a performance do código, em um primeiro momento, você pode ter escrito laço de repetição utilizado um for ou while, mas será que esse mesmo código não poderia funcionar melhor utilizando uma list comprehension ou um generator? Esses são pontos a se considerar no quesito performance, que leva em consideração não só o tempo de execução, como também o uso de recursos, como a memória, o processador, a GPU, etc.

Pensando em novos usos do sistema

Conforme falamos no início, uma boa prática é garantir que os tipos de dados passados para uma função estão corretos, ainda utilizando como exemplo a nossa calculadora, o que esperamos que seja passado para as funções são números, seja do tipo int ou float, porém pode acontecer do usuário acabar passando uma string, seja por engano porque a sua interface permite que strings também sejam digitadas para a calculadora e o usuário acabou digitando errado sem querer, ou, por que um hacker está propositalmente explorando formas de quebrar o seu sistema.

Esse é um ponto extremamente importante e que deve ser pensado com cuidado, pois no caso de uma aplicação que recebe dados do frontend e que vão ser processados no backend, pode aqui entrar as graves falhas de segurança como um SQL Injection, onde um usuário mal intencionado pode enviar comandos em um sistema mal projetado para conseguir acesso ao banco de dados, podendo obter informações confidenciais, inserir ou deletar informações.

Uma forma de verificar se o tipo de dado está correto é criar um teste utilizando o assertRaises da seguinte forma:


Adicionando esse teste e rodando novamente o comando no terminal, vai retornar um erro informando que o erro esperado TypeError não foi retornado, isso ocorre porque no Python é possível utilizar o operador de soma para juntar duas strings, mas pensando na funcionalidade do nosso sistema, isso não faz sentido, pois uma calculadora convencional não precisa somar textos, então o erro deve ser retornado.

Para fazer o teste passar, podemos adicionar uma verificação do tipo de dado que está sendo passado no começo dentro do arquivo do código da calculadora, por exemplo:

Ferramentas de medição da cobertura dos testes

Existem ferramentas que medem a cobertura dos testes, elas são úteis para identificar partes do código que não estão sendo testadas, o que pode ajudar a nós, desenvolvedores, a escrever testes mais abrangentes e melhorar a qualidade geral do código. Os exemplos de bibliotecas incluem: coverage.py, nose-cov, pytest-cov, codecov, dentre outros.


Conclusão

Por fim, o Test-Driven Development (TDD) é uma metodologia que permite que os desenvolvedores escrevam códigos mais eficientes e menos propensos a bugs. Consiste em escrever códigos de teste antes de escrever o código real, tornando possível para os desenvolvedores terem um ciclo de desenvolvimento mais ágil.

As principais vantagens de usar o TDD são a capacidade de saber com antecedência o que o código deve fazer, uma redução no número de bugs e facilidade de manutenção. Além disso, diferentes tipos de testes podem ser usados para garantir a funcionalidade de pequenas unidades de código, desempenho e segurança. Assim como o módulo nativo do python unittest que utilizamos nesse exemplo, também existem outras como o nose, pytest, doctest e mock.

Para começar a usar o TDD, é essencial entender o problema que precisa ser resolvido e definir o contexto, funcionalidades e regras do sistema. O uso de ferramentas de cobertura de testes pode ajudar a identificar partes do código que não estão sendo testadas e melhorar a qualidade geral do código.

💡
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.