TDD Desenvolvimento Guiado por testes

A primeira pessoa a apresentar este método de desenvolvimento foi Kent Beck, autor do livro Extreme Programming, e relata em um capitulo de seu livro a necessidade de testar prematuramente, frequentemente e automaticamente, destacando que tal abordagem é necessária, pois quanto mais cedo encontrarmos erros mais barato será consertá-lo. Três anos mais tarde ele lança o livro Test Driven Development by example definindo uma estratégia de desenvolvimento que além de permitir detectar erros prematuramente, cria um ambiente de trabalho que favorece o desenho de uma boa arquitetura.

A principal característica do TDD é sua simplicidade, onde a principal regra a seguir é: criar um teste que falhe, escrever o código para o teste passar e em seguida refatorar seu código.

Perceba que não foi dito que uma linguagem é preferível que outras, nem que uma IDE é necessária para trabalhar desta forma. É claro que será necessário um framework de testes e o Kent Beck criou um que foi base para todos os outros que surgiram, o JUnit.

Certo, neste ponto começam a surgir as dúvidas, sobre como definir os métodos de testes, qual a granularidade do meu teste e por onde começar.

Um sistema sempre vai começar a partir dos requisitos ou estórias, uma boa documentação que defina quais serão os comportamentos do software.

Nós, na Sigma, descrevemos o comportamento de nosso sistema utilizando estórias. Então vamos escrever uma estória hipotética que define o comportamento de um sistema.

Eu como comprador

quero adicionar produtos comprados ao estoque

para manter o saldo em estoque atualizado;

Analisando a estória acima podemos extrair algumas entidades que o sistema de almoxarifado deve ter: Produto e Estoque. Além disso podemos visualizar os comportamentos esperados: dar entrada no estoque de um produto, consultar saldo de um produto em estoque;

Então podemos criar um teste que falhe para um dos comportamentos:

import unittest

class TestEstoque(unittest.TestCase):

    def test_estoque_deve_dar_entrada_em_produto(self):
        produto = Produto()

Este teste vai falhar, pois não existe uma classe Produto definida em nosso sistema.

import unittest


class Produto(object):
    def __init__(self):
        pass


class TestEstoque(unittest.TestCase):

    def test_estoque_deve_dar_entrada_em_produto(self):
        produto = Produto()

O código vai passar então podemos refatorar e criar corretamente um módulo e retirar o código da classe produto do module de teste;

Damos o segundo passo na implementação:

import unittest

class TestEstoque(unittest.TestCase):

    def test_estoque_deve_dar_entrada_em_produto(self):
        produto = Produto()
        estoque = Estoque()
        quantidade = 10
        estoque.entrada(produto, quantidade)

Perceba que avançamos alguns passos extras na implementação, pois temos experiência na criação de classes e podemos ser flexíveis e escrever mais trechos de código que temos fluência. Esta flexibilidade não pode ser usada em excesso, pois o principal objetivo ao escrever código que falhe é que esta falha seja fácil de ser encontrada, portanto se eu escrever muitas linhas de código eu posso gastar muito tempo consertando ele. Portanto aprecie com moderação. O código acima está falhando, pois o método entrada não está implementado.

Portando vamos implementá-lo:

class Estoque(object):

    def __init__(self):
        self._estoque = []

    def entrada(self, produto, quantidade):
        self.estoque.append({
            'produto': produto,
            'quantidade': quantidade
        })

Novamente o teste vai passar, mas como definir que este teste é válido?

Asserções!

Asserção é um método para validar se uma condição de nosso teste é válida e caso ela não seja, minha asserção não será válida e vai lançar uma exceção.

import unittest

class TestEstoque(unittest.TestCase):

    def test_estoque_deve_dar_entrada_em_produto(self):

        # Definições
        produto = Produto()
        estoque = Estoque()
        quantidade = 10

        # Ações
        estoque.baixar(produto, quantidade)

        # Asserção
        saldo = estoque.saldo(produto)
        self.assertTrue(saldo == 10)

Perceba a estrutura de um testes, eu defino o estado inicial para meu teste, tomo uma ação e verifico se meu teste chegou ao estado esperado.

Este artigo serviu de base para o screencast sobre TDD, que foi gravado e está disponível neste link, com a apresentação também disponível neste link.