TestDrivenDevelopment

UnitTests em Python

Dentre todos os aspectos da metodologia "Extreme Programming" (http://www.extremeprogramming.org), pelo menos um deles é aplicável em qualquer nível do processo de desenvolvimento, com resultados refletidos não só na melhoria do código e redução do tempo de desenvolvimento como também na facilidade de manutenção posterior do código. Esse aspecto, que estarei comentando nesse artigo é chamado de "Test-driven development" (em português, Desenvolvimento Dirigido por Testes).

Introdução

No ciclo normal de desenvolvimento, você escreve parte do código da sua aplicação e testa. Corrige os eventuais problemas e testa novamente, repetindo o processo e incrementando aos poucos até atingir a funcionalidade desejada (ou a paciência se esgotar). Esse processo tem vários problemas. O mais óbvio de todos é que normalmente a etapa de testes é relegada para um momento posterior do processo de desenvolvimento, quando já há uma base de código grande a ser testada.

Segundo os proponentes da metodologia XP, esse processo tem de ser totalmente invertido. Você deve escrever os testes primeiro, antes de escrever o código que será testado. O teste irá ditar que código você deverá escrever, ou melhor, que código você precisa escrever. Você escreve apenas o código necessário para passar no teste. Nem uma linha a mais, nem uma linha a menos.

Um rápido exemplo

Criar testes antes do código é algo aparentemente confuso à primeira vista, mas aos poucos você entenderá toda a mecânica do processo e uma vez que esteja "infectado", não conseguirá mais trabalhar sem eles. Começaremos por um exemplo simples. Digamos que numa aplicação qualquer, eu preciso de um objeto Pessoa, que armazene alguns dados de um cliente, retirados de um banco de dados, como nome, sobrenome, data de nascimento. Mais do que isso, nossa empresa tem um novo produto dirigido à uma determinada faixa etária, e nossa aplicação deve selecionar os clientes por idade.

Parece claro que precisamos de uma classe Pessoa, com atributos nome, sobrenome e nascimento, além de um método idade, que retorna o valor da idade atual do cliente. Então, se já temos uma idéia de como o objeto deve ser, vamos criar essa classe Pessoa certo ?

Errado. Vamos primeiro criar o teste que verifica o funcionamento da classe Pessoa. "Mas como ?!?" você deve estar pensando "Eu não escrevi nenhum código ainda. Eu nem sei o que estou testando.". A resposta é simples. Você sabe o que está testando. Você só acha que não sabe porque nunca pensou dessa forma.

Vamos a um exemplo mínimo do teste. Estarei usando os arquivos exemplo.py para o código e test_exemplo.py para os testes:

   1 #!/usr/bin/env python
   2 # -*- coding: UTF-8 -*-
   3 
   4 import unittest
   5 
   6 from exemplo import Pessoa
   7 
   8 
   9 class TestPessoa(unittest.TestCase):
  10     def setUp(self):
  11         self.pessoa = Pessoa("Pedro", "Werneck", (31, 12, 1981))
  12 
  13     def testAtributos(self):
  14         self.assertEqual(self.pessoa.nome, "Pedro")
  15         self.assertEqual(self.pessoa.sobrenome, "Werneck")
  16         self.assertEqual(self.pessoa.nascimento, (31, 12, 1981))
  17 
  18 
  19 if __name__ == "__main__":
  20     unittest.main()

O módulo unittest, importado logo no começo do arquivo é um port para Python do !JUnit, existente para Java, que por sua vez foi baseado em outro framework de testes para Smalltalk. A API é simples de entender e usar.

A classe TestCase é que a será usada com mais frequência. As suas classes teste devem ser subclasses dela. Todos os métodos que começam com test são tratados pela classe como testes que devem ser executados. Os métodos setUp() e tearDown() são executados antes de cada um dos testes da classe.

O método assertEqual() recebe dois argumentos para testar igualdade, falhando o teste caso não sejam iguais. É mais ou menos o equivalente de assert a == b.

A função unittest.main() executa todos os métodos teste em subclasses de unittest.TestCase que forem encontrados no arquivo. Existem maneiras mais sofisticadas de executá-los, mas foge do intuito deste artigo que é ser apenas uma introdução rápida ao conceito.

Executando este exemplo acima temos a falha esperada:

Traceback (most recent call last):
  File "test_exemplo.py", line 6, in ?
    from exemplo import Pessoa
ImportError: No module named exemplo

Agora que temos um teste podemos escrever algum código, apenas o suficiente para passar no teste. Vou criar o módulo exemplo.py e criar a classe Pessoa.

   1 #!/usr/bin/env python
   2 # -*- coding:UTF-8 -*-
   3 
   4 class Pessoa:
   5     def __init__(self, nome, sobrenome, nascimento):
   6         self.nome = nome
   7         self.sobrenome = sobrenome
   8         self.nascimento = nascimento

É só isso. Lembre-se que nosso objetivo é sempre de apenas passar no teste. Nada a mais, nada a menos. Agora já temos um resultado bem diferente quando executamos nosso teste novamente:

----------------------------------------------------------------------
Ran 1 tests in 0.001s

OK

É esse sempre nosso objetivo. Conseguir um OK. Testar a existência e valor de atributos como no exemplo, não é muito importante, a menos que você esteja tentando resolver algum problema envolvendo esses atributos.

Seguindo o ciclo de desenvolvimento, agora que temos o código passando no teste, podemos acrescentar algumas linhas ao arquivo test_exemplo.py. Vamos agora testar o método idade() que deve calcular minha idade baseado na data de nascimento e na data atual.

   1 #!/usr/bin/env python
   2 # -*- coding: UTF-8 -*-
   3 
   4 import unittest
   5 
   6 from exemplo import Pessoa
   7 
   8 
   9 class TestPessoa(unittest.TestCase):
  10     def setUp(self):
  11         self.pessoa = Pessoa("Pedro", "Werneck", (31, 12, 1981))
  12 
  13     def testAtributos(self):
  14         self.assertEqual(self.pessoa.nome, "Pedro")
  15         self.assertEqual(self.pessoa.sobrenome, "Werneck")
  16         self.assertEqual(self.pessoa.nascimento, (31, 12, 1981))
  17 
  18     def testIdade(self):
  19         self.assertEqual(self.pessoa.idade(), 21)
  20         
  21 if __name__ == "__main__":
  22     unittest.main()

Minha idade atual é de 21 anos, mas como esperado, o teste falha antes mesmo de chegar ao valor, pois o método idade() ainda nem existe:

.E
======================================================================
ERROR: testIdade (__main__.TestPessoa)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 19, in testIdade
AttributeError: Pessoa instance has no attribute 'idade'

----------------------------------------------------------------------
Ran 2 tests in 0.006s

FAILED (errors=1)

Agora podemos retornar ao código. O que queremos é que ele pegue o ano da data de nascimento e calcule a idade a partir do ano atual. Para isso vamos usar a função gmtime(), contida no módulo time. Podemos calcular a idade atual subtraindo o ano de nascimento do ano atual.

   1 #!/usr/bin/env python
   2 # -*- coding:UTF-8 -*-
   3 
   4 import time
   5 
   6 class Pessoa:
   7     def __init__(self, nome, sobrenome, nascimento):
   8         self.nome = nome
   9         self.sobrenome = sobrenome
  10         self.nascimento = nascimento
  11 
  12     def idade(self):
  13         return time.gmtime()[0] - self.nascimento[2]

Como sempre, escrevemos apenas o mínimo de código necessário para passar no teste. Depois de escrever o código, podemos executar o teste novamente:

.F
======================================================================
FAIL: testIdade (__main__.TestPessoa)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 19, in testIdade
  File "/usr/lib/python2.3/unittest.py", line 302, in failUnlessEqual
    raise self.failureException, \
AssertionError: 22 != 21

----------------------------------------------------------------------
Ran 2 tests in 0.014s

FAILED (failures=1)

Aqui temos o primeiro problema capturado pelos nossos testes. Minha idade é 21 anos, não 22. Usar somente o ano para calcular a idade não foi boa idéia, já que eu ainda vou completar 22 anos no final desse ano.

Obviamente o problema tem de estar nas duas linhas que eu acabei de adicionar. Nesse caso simples, isso é óbvio, mas quando estamos lidando com projetos grandes, nem sempre fica claro que parte do código está causando o problema. Desenvolvendo o código através de testes você sempre sabe onde o problema está: na sua última alteração, entre o último teste que passou e o último que falhou. É por esse motivo que é importante desenvolver o código em passos pequenos, testando antes e depois de inserir uma alteração.

   1 #!/usr/bin/env python
   2 # -*- coding:UTF-8 -*-
   3 
   4 import time
   5 
   6 class Pessoa:
   7     def __init__(self, nome, sobrenome, nascimento):
   8         self.nome = nome
   9         self.sobrenome = sobrenome
  10         self.nascimento = nascimento
  11 
  12     def idade(self):
  13         ano_a, mes_a = time.gmtime()[:2]
  14         mes_n, ano_n = self.nascimento[1:]
  15         idade = ano_a - ano_n
  16 
  17         if mes_a < mes_n:
  18             return idade -1
  19 
  20         return idade

Aqui corrigimos o problema causado por utilizar apenas o ano no cálculo da idade, acrescentando o mês de nascimento ao cálculo. Comparando o mês de nascimento com o mês atual, podemos saber se a Pessoa já fez aniversário no ano atual e saber sua idade com maior precisão. Mas ainda temos que executar o teste para saber se funciona:

..
----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK

OK. O teste passou, pelo menos no código atual. É lógico que ele ainda pode ter problemas, mas mais importante do que mostrar o que está correto ou não, a intenção deste exemplo trivial é dar uma idéia de como é programar utilizando unittests. Eu escrevi apenas o código necessário para passar em cada teste a cada passo. Ao escrever cada teste, eu tratei cada parte do código como se já existisse, da forma como eu quero que ele seja. É um grande exercício de aprendizado.

Por que escrever testes antes ?

XXXX

Desculpas mais frequentes

Se você é um desenvolvedor solitário, não é díficil adotar unittests em seus projetos. No entanto, se você faz parte de uma equipe, é díficil manter qualquer política que exija uma certa disciplina de todos os envolvidos caso eles não concordem.

As desculpas mais comuns (e alguns contra-argumentos) são:

XXXX

Algumas Regras Para Unittesting

XXXX

Referências

1. PyUnit - the standard unit testing framework for Python

2. Learning to Love Unit Testing - Dave Thomas, Andy Hunt

3. Demystifying Extreme Programming: Test-driven programming - Roy W. Miller

4. Test Driven Development: By Example - Kent Beck - ISBN: 0321146530

5. Dive Into Python - Unit Testing


PedroWerneck

TestDrivenDevelopment (editada pela última vez em 2008-09-26 14:05:48 por localhost)