Nos testes nós confiamos: TDD com Python


Uma breve introdução sobre Desenvolvimento Orientado a Testes (Test-Driven Development) com Python!


Esse texto foi baseado em uma palestra que fiz durante a Python Nordeste 2017. Eu espero que vocês gostem! Por favor, não esqueça de dar um feedback.


Então você quer escrever uns testes, não é?

Há um tempo atrás, quando eu estava começando a minha carreira como programadora, eu ouvia outros programadores falarem sobre duas coisas: refatorar e testes unitários. Para ser honesta, eles apenas falavam sobre refatorar para explicar porquê essa prática deveria ser evitada (e o quão eles tinham medo de fazer isso) e sobre testes unitários para dizer que era muito custoso para começar, que perde-se muito tempo etc. Testes unitários soavam como uma utopia.

Quanto iniciante, eu não sabia o que pensar. Alguns anos depois, apesar de me sentir sempre começando, eu gostaria de dar a você uma introdução breve sobre TDD com Python e como escrever testes unitários e refatorar de maneira segura.

Testes unitários e TDD?

Provavelmente existem milhões de textos sobre esse assunto. Mas vamos falar só um pouquinho sobre ele sob o meu ponto de vista! 😅

Testes unitários são pedaçoes de código que exercitam uma entrada, a saída e o comportamento do seu código. Você pode escrevê-los a qualquer momento que queira. É um código para testar outro código.

Mas Desenvolvimento Orientado a Testes (Test-Driven Development) é uma estratégia para pensar (e escrever!) testes primeiro.

Deixe-me explicar isso melhor com um exemplo (finalmente o código!).

O freela

Imagine que um cliente tem um site e através desse site ele recebe vários contatos de consumidores em potencial. Depois de um tempo, seu cliente percebeu que é importante pro negócio identificar o perfil do consumidor: sua idade, gênero, emprego e outras coisas. Mas o site recebe apenas o nome e o e-mail.

Eles te contrataram para identificar o gênero da pessoa baseado em seu nome. Para a sua sorte, existe uma API maravilhosa chamada Genderize.io que identifica os possíveis gêneros. Então você rapidamente desenvolveu a conexão com essa API:

requests.get('https://api.genderize.io/?name=ana')

Entretanto o seu cliente pediu que você escrevesse testes unitários e você já estava curioso sobre o TDD. Aqui a nossa jornada começa!

Começando aos poucos (ou com baby steps)

A API é bem direta e o seu trabalho estava quase pronto. Mas com TDD nós precisamos pensar sobre os testes antes. E estar tranquilo com a possibilidade do início ser difícil algumas vezes - e está tudo bem. Sério.

Voltando ao código e pensando em começar aos poucos, qual é o menor teste que nós podemos escrever para uma função (método/classe) que irá retornar o gênero?

Tempo para você pensar

Apenas recapitulando: nós temos um nome como entrada e nós precisamos retornar o gênero como saída. Então, o menor teste é: dado um nome, retorne um gênero.

Entrada: Ana [nome] Saída: female [gênero]

-Hum… Nós vamos escrever um teste apenas para checar se dado Ana deve retornar female?

-Exatamente!

-Mas não existe nenhum código ainda!

-Não, não existe!

-😵

Aspectos importantes sobre os testes unitários

Vamos escrever o nosso primeiro teste!

def test_should_return_female_when_the_name_is_from_female_gender():
	detector = GenderDetector()
	expected_gender = detector.run('Ana')

	assert expected_gender == 'female'

Temos aqui alguns detalhes para prestar atenção. A primeira é o nome do teste. Os testes devem ser considerados como uma documentação viva. Você precisa ser descritivo ao escrevê-los e passar a mensagem de qual comportamento esperado e o que está sendo testado. Nesse caso nós explicitamente dizemos: deve retornar female quando o nome é de uma mulher.

O nome do arquivo de teste deve seguir o mesmo nome do módulo. Por exemplo, se o seu módulo chama-se gender.py, o nome do seu teste deve ser test_gender.py. O ideal é separar a sua pasta de testes do seu código de produção (a implementação) e ter algo como isso:

mymodule/
-- module.py
-- another_folder/
---- another_module.py
tests/
-- test_module.py
-- another_folder/
---- test_another_module.py

Outro detalhe importante é a estrutura. Uma convenção bastante usada é a AAA: Arrange (organize), Act (aja) and Assert (confira).

  • Arrange: você precisa organizar os dados necessários para executar aquele pedaço de código (entrada);
  • Act: aqui você irá executar o código sendo testado (exercitar o comportamento);
  • Assert: depois da execução do código, você irá conferir/checar se o resultado (saída) é a mesma que você estava esperando.

Agora você pode executar os testes. Eu sugiro a biblioteca pytest para isso. Mas você é livre para escolher qualquer uma.

Viva! Nós temos o nosso primeiro teste. Ele é lindo mas está falhando. E isso é maravilhoso!

O Ciclo

Espero que você não tenha desistido desse texto porquê agora estamos em um momento de falar sobre uma coisa importante sobre o TDD: o ciclo.

O ciclo é feito de três passos:

  • 🆘 Escreva um teste unitário e faça ele falhar (ele precisa falhar porquê a funcionalidade não existe, certo? Se o teste passar antes, chame os Caça-Fantasmas, sério)
  • ✅ Escreva a funcionalidade e faça o teste passar! (você pode fazer uma dancinha depois disso)
  • 🔵 Refatore o código - a primeira versão não precisa ser a mais bonita (não seja tímido)

Começando por partes, aos poucos, você pode utilizar esse ciclo todas as vezes que você adicionar ou modificar uma funcionalidade no seu código.

E falando sobre funcionalidade… Vamos exercitar o ciclo!

Nós fizemos nosso teste falhar. Ótimo! Agora é a hora de implementar a funcionalidade. Pensando em começar por partes, nossa implementação deve seguir a mesma regra, certo? Então, o que precisamos fazer para o teste passar? Não pense sobre a funcionalidade completa, apenas sobre o teste.

Tempo para você pensar

Nós precisamos apenas escrever o método que retorne a resposta correta: FEMALE!

def run(self, name):
 	return 'female'

Rode os testes de novo. Está verde!!! 🍀

Tudo bem, parece muito estranho e provavelmente você pensa que eu endoidei. Mas pense sobre fazer isso aos poucos… Agora nós precisamos escrever cada parte da funcionalidade em um teste diferente.

TDD não é sobre o dinheiro testes

Mais do que verificar, nós precisamos pensar sobre o design da aplicação primeiro.

Uma das coisas que me impressiona no TDD é como conseguimos desenvolver o design da aplicação bem e de maneira consciente, construindo apenas o que necessário para os testes passarem. Quando nós estamos escrevendo testes, somos forçados a pensar sobre o design primeiro e como nós podemos quebrá-lo em peças menores.

Vamos escrever mais um teste. Além dos nomes femininos, precisamos identificar nomes masculinos também.

def test_should_return_male_when_the_name_is_from_male_gender():
	detector = GenderDetector()
	expected_gender = detector.run('Pedro')

	assert expected_gender == 'male'

Mas quando nós executamos esse teste ele vai falhar porquê nós retornamos apenas female, certo? Vamos corrigir isso usando o nosso código real oficial™.

import requests


def run(self, name):
	result = requests.get('https://api.genderize.io/?name={}'.format(name))
 	return result['gender']

Agora os nossos testes estão passando! Viva!

Nós temos outros cenários para cobrir, como nomes vazios, exceções causadas pela API etc. Mas isso vai ficar como dever de casa para você.

Lições Aprendidas

Espero que você tenha se divertido! Para lembrar:

  • A grande vantagem do TDD é construir o design da aplicação primeiro
  • Seu código será mais confiável: após cada modificação você pode rodar os testes e ficar em paz
  • O início pode ser difícil - e está tudo bem. Você só precisa praticar!

O exemplo utilizado nesse texto, com mais alguns testes, está disponível nesse repositório no Github.

Alguns livros para mergulhar no TDD:

Divirta-se!


Nós temos um problema com esse código. Todas as vezes que executamos ele, o código faz requisições reais a API e isso leva algum tempo. Nós vamos aprender como lidar com esse problema no próximo texto sobre Mocks.


comments powered by Disqus