September 30th, 2006TDD em Combate - Parte 3

Apesar da demora, o desenvolvimento do jogo continuou. Neste post falaremos um pouco sobre a implementação das regras de movimentação, mostrando como novos conceitos surgem com a evolução natural do design e com a constante preocupação com a clareza e a simplicidade.

Refatorando um pouco antes de seguir em frente

Desde o último post, muitas coisas aconteceram (dentre elas minha ida ao Agile 2006) e aprendi muitas coisas. Uma das mais importantes descobertas foi a relevância dos nomes dos testes. Após ficar um tempo sem mexer no código do jogo, olhei para os nomes dos métodos de teste e não entendi o que eles faziam. Precisei olhar para o código para entender. É muito importante que o nome dos métodos de teste evidenciem não apenas o cenário do teste, mas também as expectativas sobre o comportamento que está sendo testado. Quando um teste falhar no futuro, se o nome do teste for bom, você não precisará olhar o código para entender o problema. Existe inclusive uma ferramenta Java muito simples porém muito poderosa, o Agiledox, que transforma os nomes dos métodos de uma classe de teste JUnit numa frase, criando uma documentação simples a partir do código.

Além disso, pensando na legibilidade do código de teste, resolvemos criar uma subclasse especial da peça, com métodos auxiliares que verificam o comportamento esperado numa vitória, num empate e numa derrota. Por fim, descobrimos um pequeno erro nos testes com exceções esperadas: era preciso chamar self.fail() ao invés de fail() para utilizar o método da superclasse unittest.TestCase (desculpem minha falta de conhecimento de Python, ainda estou aprendendo). O código de teste refatorado ficou assim:

# StrategoTest.py
 
import unittest
import unittestgui
import Stratego
from Stratego import InvalidPiece, InvalidAttack
 
class Piece(Stratego.Piece):
def winsAgainst(self, defendingPiece):
return self.attack(defendingPiece) > 0
def losesAgainst(self, defendingPiece):
return self.attack(defendingPiece) < 0
def tiesWith(self, defendingPiece):
return self.attack(defendingPiece) == 0
  class CreatePieceTest(unittest.TestCase): def testCreateSoldierWithRank2(self): assert Piece("soldier").rank == 2 def testCreateMinerWithRank3(self): assert Piece("miner").rank == 3 def testCreateSergeantWithRank4(self): assert Piece("sergeant").rank == 4 def testCreateLieutenantWithRank5(self): assert Piece("lieutenant").rank == 5 def testCreateCaptainWithRank6(self): assert Piece("captain").rank == 6 def testCreateMajorWithRank7(self): assert Piece("major").rank == 7 def testCreateColonelWithRank8(self): assert Piece("colonel").rank == 8 def testCreateGeneralWithRank9(self): assert Piece("general").rank == 9 def testCreateMarshalWithRank10(self): assert Piece("marshal").rank == 10 def testCreateSpyWithRank1(self): assert Piece("spy").rank == 1 def testCreateBombWithRank11(self): assert Piece("bomb").rank == 11 def testCreateFlagWithRank0(self): assert Piece("flag").rank == 0 def testCreateInvalidPieceRaises(self): try: Piece("invalid") except InvalidPiece: pass else:
self.fail("expected InvalidPiece exception")
  class AttackTest(unittest.TestCase): def testHigherRankWins(self):
assert Piece("sergeant").winsAgainst(Piece("soldier"))
def testLowerRankLoses(self):
assert Piece("miner").losesAgainst(Piece("colonel"))
def testEqualRankTies(self):
assert Piece("major").tiesWith(Piece("major"))
def testSpyWinsAgainstMarshal(self):
assert Piece("spy").winsAgainst(Piece("marshal"))
def testMarshalWinsAgainstSpy(self):
assert Piece("marshal").winsAgainst(Piece("spy"))
def testMinerWinsAgainstBomb(self):
assert Piece("miner").winsAgainst(Piece("bomb"))
def testBombCannotAtack(self): try: Piece("bomb").attack(Piece("miner")) except InvalidAttack: pass else:
self.fail("expected InvalidAttack exception")
def testFlagCannotAtack(self): try: Piece("flag").attack(Piece("lieutenant")) except InvalidAttack: pass else:
self.fail("expected InvalidAttack exception")
  # (...) Main Method

 
Como uma lição final aprendida com o erro acima, devemos sempre olhar o motivo de um teste estar vermelho. Nesse caso, ele ficava vermelho devido ao erro de sintaxe, pois o interpretador Python não encontrava o método fail() quando a exceção não era lançada. Ao fazer o teste passar lançando a exceção esperada, fazemos com que o teste nunca caia na chamada ao método fail(). Por ser uma linguagem dinâmica, o interpretador de Python não reclama pois nem precisa executar a linha errada.

Evoluindo o design para implementar a movimentação

Conforme discutimos no primeiro post, as regras de movimentação são simples: todas as peças podem dar 1 passo em qualquer direção vertical ou horizontal, exceto o Soldado (que pode andar mais que 1 passo), a Bomba e a Bandeira (que não podem andar).

É hora de pensarmos um pouco sobre o design para implementar a movimentação. Uma primeira idéia era fazer a peça guardar sua posição atual no tabuleiro e representar um movimento com uma chamada a um método que receberia a posição final da peça. O problema com essa abordagem é que a peça ficaria com muito mais responsabilidade que ela deveria. Uma peça precisa saber sobre coordenadas do tabuleiro? Como o tabuleiro saberia qual peça está em qual posição na hora de verificar movimentos inválidos ou situações de ataque? Pensando nas dependências entre os objetos, nos parece muito mais sensato o tabuleiro conhecer as peças. Porém, conforme já verificamos, algumas peças possuem regras especiais de movimentação. Então, apesar da peça não precisar saber sobre coordenadas, precisaremos de alguma forma perguntar para a peça se o “tamanho” do movimento que estamos tentando realizar é válido.

Foi nessa hora que tivemos um daqueles momentos “Eureka”: “Podemos representar o movimento como um vetor geométrico aplicado à um ponto num plano cartesiano”. Com isso, podemos calcular a magnitude do vetor e perguntar para a peça se aquele movimento é de “tamanho” válido. Infelizmente, TDD não ensina como ter esses momentos de insight, mas todos nós sabemos que programar é uma atividade que requer criatividade e treino. Na minha opinião, duas práticas de XP são o segredo para facilitar o surgimento desses momentos: Trabalho Energizado (antigamente chamado de Ritmo Sustentável ou Semana de 40 Horas) e Programação Pareada. Quantas vezes você já não ficou trabalhando até tarde em cima do mesmo problema e, ao chegar na manhã seguinte, descobre a solução trivial? Além disso, é incrível a quantidade de vezes que me surpreendi em discussões sobre um problema específico. Muitas vezes a discordância entre os pares termina com o surgimento de uma nova solução, muito melhor do que todas as outras pensadas anteriormente (e que originaram a discussão).

Uma pequena revisão da aula de geometria e vetores: um vetor num espaço euclidiano de n dimensões pode ser representado como uma combinação linear de n vetores unitários e ortonormais (também conhecidos como base). Num espaço bi-dimensional, essa base é formada pelos vetores geradores dos eixos x e y. Sendo i e j os vetores unitários paralelos aos eixos x e y respectivamente, podemos representar qualquer outro vetor a nesse espaço 2D como uma tupla (a1, a2) onde a = a1i + a2j.

Como os movimentos no jogo são apenas na vertical ou na horizontal, criamos o que chamamos de vetores ortogonais. Vetores ortogonais devem ter pelo menos um componente nulo, seja na vertical ou na horizontal. Com isso já podemos continuar com os próximos testes.

Vetores num tabuleiro 2D

Primeiramente vamos fazer um teste para verificar a criação de um vetor horizontal. Para isso, criamos uma nova classe de teste e a incluímos na suite de testes:

# StrategoTest.py
 
class OrthogonalVectorTest(unittest.TestCase):
    def testCreateHorizontalVector(self):
        assert OrthogonalVector(42, 0).y == 0
 
suite = unittest.TestSuite((unittest.makeSuite(CreatePieceTest),
                            unittest.makeSuite(AttackTest),
unittest.makeSuite(OrthogonalVectorTest)
)) # (...) Main Method

Para fazer esse teste passar, basta criar uma nova classe OrthogonalVector e importá-la no módulo de teste:

# Stratego.py
 
class OrthogonalVector:
    def __init__(self, x, y):
        self.y = 0

A implementação para os próximos dois testes segue o mesmo raciocínio, então vou pular os passos intermediários e mostrar os novos testes no verde:

# StrategoTest.py
 
class OrthogonalVectorTest(unittest.TestCase):
    def testCreateHorizontalVector(self):
        assert OrthogonalVector(42, 0).y == 0
def testCreateVerticalVector(self):
assert OrthogonalVector(0, 42).x == 0
def testNonOrthogonalVectorRaises(self):
try:
OrthogonalVector(1, 2)
except InvalidVector:
pass
else:
self.fail("expected InvalidVector exception")
  # Stratego.py - (...)   class OrthogonalVector: def __init__(self, x, y):
if 0 not in [x, y]:
raise InvalidVector
self.x = 0
self.y = 0 # (...)   class InvalidVector(Exception): pass

Nosso vetor ainda não armazena os valores esperados no construtor. Lembrem-se que a implementação para deixar o teste verde deve ser rápida. O próximo teste que falha verifica o tamanho de um vetor com componentes positivas:

# StrategoTest.py - (...) class OrthogonalVectorTest
 
    def testCalculatePositiveMagnitude(self):
        assert OrthogonalVector(3, 0).magnitude() == 3
# (...)

A implementação ainda não pede pelo armazenamento dos componentes do vetor, uma vez que podemos devolver uma constante no novo método:

# Stratego.py - (...) class OrthogonalVector
 
    def magnitude(self):
        return 3
# (...)

O próximo teste para calcular o tamanho de um vetor com componentes negativos finalmente pede pela implementação que armazena os valores dos componentes no construtor:

# StrategoTest.py - (...) class OrthogonalVectorTest
 
def testCalculateNegativeMagnitude(self):
assert OrthogonalVector(0, -42).magnitude() == 42
  # Stratego.py - (...) class OrthogonalVector   def __init__(self, x, y): if 0 not in [x, y]: raise InvalidVector
self.x = x
self.y = y
def magnitude(self):
return abs(self.x + self.y)
# (...)

Essa técnica de implementação que acabamos de utilizar é chamada de triangulação. Para aplicá-la, você deve escrever mais de um teste com exemplos diferentes a respeito do mesmo comportamento. Por exemplo, num método que soma 2 + 2, para evitar que o resultado seja sempre 4, você pode escrever outro teste que verifica se soma(3, 4) é igual a 7. Reparem também que nossa implementação não é completamente correta. Aqueles que estudaram geometria devem estar pensando: “Por que estamos calculando a norma do vetor como a soma de seus componentes?”, quando na verdade deveria ser a raíz da soma dos quadrados dos componentes. Lembrem-se da importância do design simples e evolutivo. Não tentem incluir complexidade além da necessária. No nosso caso particular, como um dos componentes é sempre nulo por construção, podemos considerar que a norma do vetor é a soma dos componentes.
 

As peças aprendem o quanto podem se movimentar

Agora que já temos uma implementação simples de vetores, podemos começar a pensar nas regras que verificam o quanto cada peça pode se mover. Para isso, escrevemos o nosso próximo teste que verifica a regra geral de movimentação, implementando o mínimo para fazê-lo passar. Por padrão, todas as peças podem dar 1 passo em qualquer direção:

# StrategoTest.py
 
class MovementTest(unittest.TestCase):
def testSergeantCanMoveOneStep(self):
assert Piece("sergeant").canMove(1)
  suite = unittest.TestSuite((unittest.makeSuite(CreatePieceTest), unittest.makeSuite(AttackTest), unittest.makeSuite(OrthogonalVectorTest),
unittest.makeSuite(MovementTest)
)) # (...) Main Method   # Stratego.py - (...) class Piece  
def canMove(self, steps):
return True
  # (...) OrthogonalVector/Exceptions

Os próximos dois testes garantem que a bomba e a bandeira não podem andar. Mais uma vez vou mostrar os testes e a implementação, agora que vocês já entenderam o ritmo de TDD. Primeiro vemos o teste falhar, depois implementamos o mínimo para fazê-lo passar para depois refatorar e melhorar o design:

# StrategoTest.py - (...) class MovementTest
 
def testBombCannotMove(self):
assert not Piece("bomb").canMove(1)
def testFlagCannotMove(self):
assert not Piece("flag").canMove(1)
# (...)   # Stratego.py - (...) class Piece   def canMove(self, steps):
if self.rank in [11, 0]:
return False
return True # (...) OrthogonalVector/Exceptions

Por enquanto a implementação ainda não está completa. Precisamos de um teste para garantir que uma peça normal não pode dar mais que 1 passo:

# StrategoTest.py - (...) class MovementTest
 
def testMajorCannotMoveMoreThanOneStep(self):
assert not Piece("major").canMove(2)
# (...)

A implementação para fazer o teste passar pode ser feia a princípio…

# Stratego.py - (...) class Piece
 
    def canMove(self, steps):
if self.rank in [11, 0] or (self.rank == 7 and steps > 1):
return False return True # (...)

… contanto que a refatoração seja feita depois que a barra de testes está verde. Nesse caso, resolvemos criar uma classe auxiliar para armazenar as propriedades de uma peça (por enquanto o rank e o número máximo de passos que ela pode dar), modificando o dicionário de inicialização, o construtor e o método canMove():

# Stratego.py
 
class PieceProperties:
def __init__(self, rank, maxSteps=1):
self.rank = rank
self.maxSteps = maxSteps
  class Piece:
__pieces = {"flag": PieceProperties(0, 0),
"spy": PieceProperties(1),
"soldier": PieceProperties(2),
"miner": PieceProperties(3),
"sergeant": PieceProperties(4),
"lieutenant": PieceProperties(5),
"captain": PieceProperties(6),
"major": PieceProperties(7),
"colonel": PieceProperties(8),
"general": PieceProperties(9),
"marshal": PieceProperties(10),
"bomb": PieceProperties(11, 0)}
__winsAttacking = [(1,10), (3,11)] __cannotAttack = [0,11]   def __init__(self, name): try:
self.rank = self.__pieces[name].rank
self.maxSteps = self.__pieces[name].maxSteps
except KeyError: raise InvalidPiece()   # (...) attack() Method  
def canMove(self, steps):
return steps <= self.maxSteps
  # (...) OrthogonalVector/Exceptions

O próximo teste verifica a última exceção: o soldado pode dar quantos passos quiser, numa mesma direção. Para a implementação, resolvemos utilizar um valor negativo, que significa “não há número máximo de passos para essa peça”:

# StrategoTest.py - (...) class MovementTest
 
def testSoldierCanMoveMoreThanOneStep(self):
assert Piece("soldier").canMove(42)
# Stratego.py - (...) class Piece   __pieces = {"flag": PieceProperties(0, 0), "spy": PieceProperties(1),
"soldier": PieceProperties(2,-1),
"miner": PieceProperties(3), # (...)   def canMove(self, steps):
return steps <= self.maxSteps or self.maxSteps == -1
  # (...) OrthogonalVector/Exceptions

O último teste verifica o cenário de erro numa tentativa de andar passos negativos. A implementação é simples: criamos uma nova exceção e verificamos o erro no método canMove():

# StrategoTest.py - (...) class MovementTest
 
def testMoveNegativeStepsRaises(self):
try:
Piece("lieutenant").canMove(-1)
except IllegalStep:
pass
else:
self.fail("expected IllegalStep exception")
  # Stratego.py - (...) class Piece   def canMove(self, steps):
if steps <= 0:
raise IllegalStep
return steps <= self.maxSteps or self.maxSteps == -1   # (...) OrthogonalVector/Exceptions  
class IllegalStep(Exception):
pass

Conclusões e próximos passos

Terminamos o terceiro post com 123 linhas de código de teste e 63 linhas de código de produção. Uma verificação de cobertura de código nos mostra que estamos 100% cobertos. Esse é um dos “efeitos colaterais” de TDD: além de ganhar confiança no código que está escrevendo, você ainda termina com uma bateria de testes que cobre 100% do seu código. No próximo post iremos evoluir nossos vetores para implementar as regras de movimentação no tabuleiro. A seguir, um resumo do que aprendemos nesse post e o código final:
 

  • Conhecemos a importância dos nomes que damos aos testes
  • Aprendemos a verificar o que está errado quando estamos no vermelho
  • Conversamos um pouco sobre dependências e divisão de responsabilidades entre objetos
  • Discutimos outras práticas de XP, como Trabalho Energizado e Programação Pareada
  • Implementamos uma classe auxiliar para representar vetores e facilitar nossa implementação das regras de movimentação

Atualização 03-Out-06: Conforme sugestões, estou disponibilizando para download o código fonte final dos testes e das classes de produção para os interessados não precisarem copiar/colar/formatar tudo novamente.

# StrategoTest.py
 
import unittest
import unittestgui
import Stratego
from Stratego import InvalidPiece, InvalidAttack, InvalidVector, IllegalStep
from Stratego import OrthogonalVector
 
class Piece(Stratego.Piece):
    def winsAgainst(self, defendingPiece):
        return self.attack(defendingPiece) > 0
    def losesAgainst(self, defendingPiece):
        return self.attack(defendingPiece) < 0
    def tiesWith(self, defendingPiece):
        return self.attack(defendingPiece) == 0
 
class CreatePieceTest(unittest.TestCase):
    def testCreateSoldierWithRank2(self):
        assert Piece("soldier").rank == 2
    def testCreateMinerWithRank3(self):
        assert Piece("miner").rank == 3
    def testCreateSergeantWithRank4(self):
        assert Piece("sergeant").rank == 4
    def testCreateLieutenantWithRank5(self):
        assert Piece("lieutenant").rank == 5
    def testCreateCaptainWithRank6(self):
        assert Piece("captain").rank == 6
    def testCreateMajorWithRank7(self):
        assert Piece("major").rank == 7
    def testCreateColonelWithRank8(self):
        assert Piece("colonel").rank == 8
    def testCreateGeneralWithRank9(self):
        assert Piece("general").rank == 9
    def testCreateMarshalWithRank10(self):
        assert Piece("marshal").rank == 10
    def testCreateSpyWithRank1(self):
        assert Piece("spy").rank == 1
    def testCreateBombWithRank11(self):
        assert Piece("bomb").rank == 11
    def testCreateFlagWithRank0(self):
        assert Piece("flag").rank == 0
    def testCreateInvalidPieceRaises(self):
        try:
            Piece("invalid")
        except InvalidPiece:
            pass
        else:
            self.fail("expected InvalidPiece exception")
 
class AttackTest(unittest.TestCase):
    def testHigherRankWins(self):
        assert Piece("sergeant").winsAgainst(Piece("soldier"))
    def testLowerRankLoses(self):
        assert Piece("miner").losesAgainst(Piece("colonel"))
    def testEqualRankTies(self):
        assert Piece("major").tiesWith(Piece("major"))
    def testSpyWinsAgainstMarshal(self):
        assert Piece("spy").winsAgainst(Piece("marshal"))
    def testMarshalWinsAgainstSpy(self):
        assert Piece("marshal").winsAgainst(Piece("spy"))
    def testMinerWinsAgainstBomb(self):
        assert Piece("miner").winsAgainst(Piece("bomb"))
    def testBombCannotAtack(self):
        try:
            Piece("bomb").attack(Piece("miner"))
        except InvalidAttack:
            pass
        else:
            self.fail("expected InvalidAttack exception")
    def testFlagCannotAtack(self):
        try:
            Piece("flag").attack(Piece("lieutenant"))
        except InvalidAttack:
            pass
        else:
            self.fail("expected InvalidAttack exception")
 
class OrthogonalVectorTest(unittest.TestCase):
    def testCreateHorizontalVector(self):
        assert OrthogonalVector(42, 0).y == 0
    def testCreateVerticalVector(self):
        assert OrthogonalVector(0, 42).x == 0
    def testNonOrthogonalVectorRaises(self):
        try:
            OrthogonalVector(1, 2)
        except InvalidVector:
            pass
        else:
            self.fail("expected InvalidVector exception")
    def testCalculatePositiveMagnitude(self):
        assert OrthogonalVector(3, 0).magnitude() == 3
    def testCalculateNegativeMagnitude(self):
        assert OrthogonalVector(0, -42).magnitude() == 42
 
class MovementTest(unittest.TestCase):
    def testSergeantCanMoveOneStep(self):
        assert Piece("sergeant").canMove(1)
    def testBombCannotMove(self):
        assert not Piece("bomb").canMove(1)
    def testFlagCannotMove(self):
        assert not Piece("flag").canMove(1)
    def testMajorCannotMoveMoreThanOneStep(self):
        assert not Piece("major").canMove(2)
    def testSoldierCanMoveMoreThanOneStep(self):
        assert Piece("soldier").canMove(42)
    def testMoveNegativeStepsRaises(self):
        try:
            Piece("lieutenant").canMove(-1)
        except IllegalStep:
            pass
        else:
            self.fail("expected IllegalStep exception")
 
suite = unittest.TestSuite((unittest.makeSuite(CreatePieceTest),
                            unittest.makeSuite(AttackTest),
                            unittest.makeSuite(OrthogonalVectorTest),
                            unittest.makeSuite(MovementTest)
                           ))
 
if __name__ == "__main__":
    unittestgui.main("StrategoTest.suite")
 
# Stratego.py
 
class PieceProperties:
    def __init__(self, rank, maxSteps=1):
        self.rank = rank
        self.maxSteps = maxSteps
 
class Piece:
    __pieces = {"flag": PieceProperties(0, 0),
                "spy": PieceProperties(1),
                "soldier": PieceProperties(2,-1),
                "miner": PieceProperties(3),
                "sergeant": PieceProperties(4),
                "lieutenant": PieceProperties(5),
                "captain": PieceProperties(6),
                "major": PieceProperties(7),
                "colonel": PieceProperties(8),
                "general": PieceProperties(9),
                "marshal": PieceProperties(10),
                "bomb": PieceProperties(11, 0)}
    __winsAttacking = [(1,10), (3,11)]
    __cannotAttack = [0,11]
 
    def __init__(self, name):
        try:
            self.rank = self.__pieces[name].rank
            self.maxSteps = self.__pieces[name].maxSteps
        except KeyError:
            raise InvalidPiece()
 
    def attack(self, defender):
        if self.rank in self.__cannotAttack:
            raise InvalidAttack()
        if (self.rank,defender.rank) in self.__winsAttacking:
            return 1
        else:
            return self.rank - defender.rank
 
    def canMove(self, steps):
        if steps <= 0:
            raise IllegalStep
        return steps <= self.maxSteps or self.maxSteps == -1
 
class OrthogonalVector:
    def __init__(self, x, y):
        if 0 not in [x, y]:
            raise InvalidVector
        self.x = x
        self.y = y
    def magnitude(self):
        return abs(self.x + self.y)
 
class InvalidPiece(Exception):
    pass
 
class InvalidAttack(Exception):
    pass
 
class InvalidVector(Exception):
    pass
 
class IllegalStep(Exception):
    pass

É hora de codificar um pouco: mostraremos o passo-a-passo de como programar no ritmo de TDD, desenvolvendo as regras de ataque do jogo Combate. Vamos identificar também a preocupação constante com o Design Simples, e como ele se manifesta quando desenvolvemos os testes antes do código.

Pensando no Design

No último post, conhecemos as peças que compõem o jogo e as respectivas regras de ataque/defesa. Como a idéia de hoje é implementarmos essas regras de ataque, precisamos pensar um pouco sobre como modelar nossas classes (lembrem-se, é permitido pensar sobre o design antes de começar a implementação. O que XP tenta evitar é o chamado BDUF - Big Design Up-Front - onde o design do sistema inteiro é definido e escrito num documento, com diversas páginas e diagramas de classe que será lido por outra pessoa na hora da implementação). O Design Incremental de XP faz com que você se preocupe somente com a mínima quantidade de design necessária para atender suas necessidades atuais. No nosso caso, precisamos apenas pensar em onde implementar as regras de ataque.

Um ataque pode ter 3 desfechos diferentes: a peça que atacou vence e ocupa o lugar da peça de defesa (que é removida do tabuleiro); a peça que atacou perde e é removida do tabuleiro ou ambas as peças são removidas do tabuleiro no caso de um empate. Minha primeira idéia era definir um método de ataque que retornasse a peça perdedora, porém no caso do empate, eu precisaria retornar as duas peças, exigindo uma assinatura que devolvesse uma lista de peças ou um par de peças, o que não achei uma boa idéia. Tive então outra idéia para implementação: fazer com que o método de ataque avisasse cada peça perdedora, numa espécie de método callback sem retorno. Porém, como um objeto Peça faria para se remover do tabuleiro ao descobrir que perdeu? Ele precisaria conhecer o Tabuleiro ou avisar o Tabuleiro, criando dependência bi-direcional entre as classes, o que também não parece ser uma boa idéia (nem estamos pensando no design do Tabuleiro ainda, só queremos implementar as regras de ataque).

A solução sugerida, depois de algumas discussões com o RBP, foi implementar um método de comparação (no estilo de um cmp() em Python ou compareTo() em Java) que devolve 0 se houve empate, um número positivo se a peça que está atacando venceu ou um número negativo se a peça atacante perdeu. Só faltava escolher onde esse método seria definido. Minha primeira idéia foi colocá-lo numa classe Stratego que representa o jogo, porém depois de alguns minutos de codificação, percebi que o melhor lugar para definir esse método seria na própria peça. Uma peça sabe como se portar quando ataca um inimigo. A última decisão de design foi definir um rank numérico para cada peça, para representar a hierarquia utilizada durante os ataques. Com a tabela seguinte em mãos, pudemos recomeçar a implementação, no ritmo de TDD:

Rank   Peça   Rank   Peça   Rank   Peça
0   Bandeira   4   Sargento   8   Coronel
1   Espião   5   Tenente   9   General
2   Soldado   6   Capitão   10   Marechal
3   Cabo-Armeiro   7   Major   11   Bomba

Criando as peças

O primeiro passo é sempre escrever um teste que falha. Como queremos criar as peças acima, defini um conjunto de testes que verifica se as peças estão sendo criadas com o rank esperado, num arquivo chamado StrategoTest.py (que armazenará os testes):

# StrategoTest.py
import unittest
import unittestgui
from Stratego import *
 
class CreatePieceTest(unittest.TestCase):
    def testCreateSoldier(self):
        assert Piece("soldier").rank == 2
 
suite = unittest.makeSuite(CreatePieceTest)
 
if __name__ == "__main__":
    unittestgui.main("StrategoTest.suite")

Esse código não compila e não consegue ser executado. Percebam como o teste pede pela codificação: definindo um construtor para peça que recebe uma string, definindo uma propriedade rank para a peça e definindo o módulo onde a classe será definida. Com isso, precisamos do seguinte trecho de código para termos nossa primeira barra verde (definido no arquivo Stratego.py):

# Stratego.py
 
class Piece:
    def __init__(self, name):
        self.rank = 2

Essa é a implementação mais simples que faz o nosso teste passar. Percebam como o TDD nos auxilia a evitar generalizações precoces no código. Mesmo sabendo que esse código não atende a todas as necessidades, precisamos nos disciplinar e focar no mínimo necessário. O terceiro passo seria a refatoração, mas ainda não temos nenhum “mal-cheiro” no código, então podemos partir para o próximo teste:

# StrategoTest.py
 
class CreatePieceTest(unittest.TestCase):
    def testCreateSoldier(self):
        assert Piece("soldier").rank == 2
def testCreateMiner(self):
assert Piece("miner").rank == 3

Ao executar a suite de testes, nos deparamos mais uma vez com uma barra vermelha. É hora de implementarmos um pouco mais para torná-la verde:

# Stratego.py
class Piece:
    def __init__(self, name):
        if name == "soldier":
            self.rank = 2
        else:
            self.rank = 3

Alguns podem dizer que o código acima já está começando a cheirar mal, porém ainda não vejo tanta repetição, então resolvo escrever o próximo teste que falha:

# StrategoTest.py
 
class CreatePieceTest(unittest.TestCase):
    def testCreateSoldier(self):
        assert Piece("soldier").rank == 2
    def testCreateMiner(self):
        assert Piece("miner").rank == 3
def testCreateSergeant(self):
assert Piece("sergeant").rank == 4

Para fazer com que os testes fiquem verdes novamente, preciso codificar mais um pouco:

# Stratego.py
 
class Piece:
    def __init__(self, name):
        if name == "soldier":
            self.rank = 2
        elif name == "miner":
            self.rank = 3
        else:
            self.rank = 4

Agora estou numa barra verde, porém o código não parece muito bonito. Fica evidente que a cada novo teste, uma nova ramificação elif precisará ser adicionada ao código do construtor. É hora de refatorar (a regra é: “sempre refatore no verde”), deixar o código mais limpo e simples sem alterar seu comportamento, ou seja, sem quebrar nenhum dos testes existentes:

# Stratego.py
 
class Piece:
    __pieces = {"soldier":2, "miner":3, "sergeant":4}
    def __init__(self, name):
        self.rank = self.__pieces[name]

Definindo um dicionário com as possíveis peças, o código do construtor fica mais simples, pois se transforma numa simples consulta ao dicionário. Seguindo o mesmo ritmo para construir as outras peças e definindo uma exceção para a tentativa de criação de uma peça inválida, o Desenvolvimento Orientado a Testes nos leva ao seguinte código final:

# StrategoTest.py
 
import unittest
import unittestgui
from Stratego import *
class CreatePieceTest(unittest.TestCase):
    def testCreateSoldier(self):
        assert Piece("soldier").rank == 2
    def testCreateMiner(self):
        assert Piece("miner").rank == 3
    def testCreateSergeant(self):
        assert Piece("sergeant").rank == 4
    def testCreateLieutenant(self):
        assert Piece("lieutenant").rank == 5
    def testCreateCaptain(self):
        assert Piece("captain").rank == 6
    def testCreateMajor(self):
        assert Piece("major").rank == 7
    def testCreateColonel(self):
        assert Piece("colonel").rank == 8
    def testCreateGeneral(self):
        assert Piece("general").rank == 9
    def testCreateMarshal(self):
        assert Piece("marshal").rank == 10
    def testCreateSpy(self):
        assert Piece("spy").rank == 1
    def testCreateBomb(self):
        assert Piece("bomb").rank == 11
    def testCreateFlag(self):
        assert Piece("flag").rank == 0
    def testCreateInvalidPiece(self):
        try:
            Piece("invalid")
        except InvalidPiece:
            pass
        else:
            fail("expected InvalidPiece exception")
 
suite = unittest.makeSuite(CreatePieceTest)
 
if __name__ == "__main__":
    unittestgui.main("StrategoTest.suite")
 
# Stratego.py
 
class Piece:
    __pieces = {"flag":0, "spy":1, "soldier":2, "miner":3, "sergeant":4,
                "lieutenant":5, "captain":6, "major":7, "colonel":8,
                "general":9, "marshal":10, "bomb":11}
    def __init__(self, name):
        try:
            self.rank = self.__pieces[name]
        except KeyError:
            raise InvalidPiece()
 
class InvalidPiece(Exception):
    pass

Implementando o método de ataque

Agora que temos as peças criadas, podemos implementar as regras de ataque. Apenas para relembrar, as peças com maior rank vencem as de menor rank. Algumas exceções à regra são:
 

  • O espião vence o marechal quando o ataca
  • As bombas e a bandeira não podem atacar
  • Qualquer peça que atacar a bomba perde, exceto o cabo-armeiro

Para não perder o costume, começamos com um teste que falha:

# StrategoTest.py
 
class AttackTest(unittest.