In my last QCon presentation in São Paulo, I gave the audience my take on refactoring strategies. You can check out my slides from that talk on Slideshare (in Portuguese, but you should be able to follow the pictures and code samples).

One of my main points is that you need to learn how to shift between two levels of thinking when you engage in refactoring, each requiring you to develop different sets of skills. I called them: Mechanic vs. Strategic.

Mechanics is important because you should be doing small, incremental, and opportunistic refactorings all the time while you develop code. It’s not a coincidence that TDD has an explicit step of refactoring after you tests are green. In order to improve the design of your code you should master how to perform these small steps well. A lot of the modern IDEs – especially in static typed languages – provide great support for this and you should learn how to use them well.

On a strategic level, you have to be able to take a step back and understand the big picture of what you’re trying to achieve. While it’s important to know how to perform individual refactorings, it’s more important at this level to know how to compose them in order to achieve the design improvement you desire.

One of the examples I used to demonstrate this, was inspired by a blog entry that Paul Hammant published a few weeks before my talk. I got some feedback that it was hard to follow the code changes on the slides, so I decided to record a small video of me tackling his refactoring experiment.

In the following video, the strategy is not overly complex: extracting a few methods from a single controller into separate controllers. However, you will see how the order of steps you use can affect the refactoring, making your job harder or easier. You should also be able to pick up a lot of the mechanics along the way:

You can find some of my attempts in branches on this Github repository. If you want to try it out for yourself, or to take this experiment further, these are some of the things I would explore:

  • The Upseller class should have it’s own tests, and I missed the @Component and @Scope("request") annotations that would probably be caught by integration tests.
  • Using Mockito or some other mocking framework to mock collaborators, and adding more behaviour to the domain model objects since using toString for testing is not something I would particularly do in real life.
  • Perhaps I could’ve used git stash instead of git reset to save the initial refactoring steps and later reapplying it assuming the merge would be simple.
  • The path I took is not the only one and you can probably explore different refactoring steps and different approaches to get to the goal.

I would like to thank Paul Hammant for coming up with this experiment and for encouraging me to publish this screencast. Please send me some feedback if you think this was useful or helpful for you!

Post to Twitter

I’m back from a 2 week “conference season” in Brazil and Argentina, but before I share my experiences on those conferences, I want to talk about my first session at the São Paulo Coding Dojo since I left in February. It was very unusual and pleasant for different reasons: first of all, George had arrived in Brazil for Rails Summit Latin America and I took him to the session; second, it was held at one of the participant’s house, instead of the usual venue at the University of São Paulo (which made it possible to add a taste of fresh baked pizzas to the retrospective); third, we decided to try out a different session format called, in lack of a better name, ÃœberDojo which I will try to describe in this post. Hugo has already explained how they came up with the new format in his blog.

In an ÃœberDojo, the setup is different than a regular session: there are more than one laptop and no projector. The number of laptops may vary depending on the number of participants, but we were 14 and decided to have 4 pairs, so 4 laptops were available. We laid out the laptops in 2 tables and chose 2 different problems to be solved in 2 different languages. We would go with Python and Ruby, but George argued in favour of having different programming paradigms to enrich the discussions, so we ended up coding in Ruby and Haskell. The problems we chose were already familiar to most of the participants since the goal of the session was to try out the new format rather than solving a difficult problem or learning a new language: Minesweeper and Bank OCR were the chosen problems.

The dynamics of the session are similar to a Randori session, but with multiple pairs coding at the same time. We used a fixed timebox of 7 minutes to switch the pairs (when the round is over, the driver leaves the pair, the observer becomes pilot, and a new observer joins from the audience). The big difference in this format is that it’s impossible for everyone to follow the same train of thought, and there’s always more people than seats available so there’s a lot more chatting happening. When a new member joins the pair, it’s very likely that he will have almost no previous experience with the code being developed (similar to a real-life environment?), so everyone can exercise their ability to read and write readable code. In the original idea, people who were not coding were not supposed to follow what the other pairs were doing, but we ended up helping other pairs with less knowledge on the languages and/or environment. I have to admit I sometimes tried to stick around one of the pair stations because the approach to the solution and the discussions were getting interesting :-)

At the end we held our usual retrospective with food (a lot of pizza) and everyone seemed to have enjoyed the session. I particularly felt the energy and the innovation happening and was very excited with the results. We coded for more than 1:30 hours and it was very hard for me to leave the code unfinished. Although it was a lot of fun, I think for this kind of session to be successful, the participants need to be familiar with a Dojo session and its dynamics (TDD and Pair Programming in particular). Hugo was also a very good moderator, using a pairing matrix to keep track of who was pairing with who, to keep at least one experienced member on each pair, and to put different people in different pairs at each round. The code from that session is available at GitHub.

I see some benefits to trying out this type of session. Solving the same problems in different languages (and paradigms) made it possible to have a rich discussion at the end. For example: when solving the Bank OCR problem in Haskell, pattern matching made it easy to make progress on the first steps, while the Ruby version was getting stuck with a lot of nested if/else statements. Another benefit of this type of session is that you can cope with programmers in different levels of experience. In a traditional Dojo session, you always go as fast as your least experienced participant. In this session, you have different streams of development happening at the same time, so a faster pair can make more progress on their turn. Finally, I think this environment is much more similar to what you might encounter in “real-life”: you develop code for others to read and you have to understand what others were doing while you were absent.

I hope you try out this format in your Dojo and share your experiences. What other types of session are you trying out in your local Dojos? Would you have a better name for this new session format? Leave your comments!

Post to Twitter

This is a common question that usually comes up when I talk about Coding Dojos, that I found it would be useful to share it with everyone:

“Where can I find problems to solve on my Coding Dojo?”

At the São Paulo Coding Dojo, we tend to choose problems from different sources, and we keep track of the sources in a wiki. Since this page hasn’t changed for a while, I’m publishing the current version here so others can benefit:

The list is not supposed to be exhaustive, but there’s quite a lot in there to keep you busy for a while. I should highlight that both UVa and SPOJ accept code submissions so you can get feedback about whether your solution works or not. SPOJ, in particular, accepts a wide variety of languages (such as Ruby, Python, Java, C, Brainf**k, and even Whitespace)

For those interested in starting a Coding Dojo, there are some easier problems that we usually use when the audience is new to the whole Dojo concept. Also, they are not hard so the focus is not on solving the problem, but actually learning the interactions and flow of a Coding Dojo session (TDD, pair programming, or sometimes to learn a new language):

I hope this is helpful and, for more information about the São Paulo Coding Dojo, follow our blog, join our discussion list (for the Portuguese speakers), read our paper or some of our session reports in the Coding Dojo wiki (in English). Enjoy!

Post to Twitter

A recent post on InfoQ made me think about how I use mocks during development. I used to be a classicist tester and thought mocks were only useful to fake external services (usually slow or hard to setup). After learning more about interaction-based testing and BDD, I now see some good advantages of using mocks during development:

Outside-In Development

Behaviour-Driven Development is more than a variation of TDD. I like Dan North‘s explanation of BDD as an “outside-in” approach to development. We start at the Story level, defining the acceptance criteria as scenarios which create an agreement between the team and the customer on the meaning of DONE. From the outside view, we start digging into the domain model, designing the objects and services needed to implement the story. I find mocks really useful in such scenario, because I can define the interface of the objects that still doesn’t exist, while focusing on the current layer of functionality. As a side effect, my tests become more isolated and when they fail I usually have a good indication of where the problem is located, requiring less debugging.

In this sense, I think the really important scenario for using mocks from Colin Mackay’s article is the last one: “when the real object does not yet exist”. I still mock someone else’s objects in the boundaries of the system, to assert that my code interacts properly with external libraries. But I see much more value in mocking my own objects, designing interactions and isolating behaviour. Which leads to my next point…

CRC with tests

CRC cards was one of the techniques that thought me what good Object-Oriented design looks like. It focus on designing behaviour-rich objects, defining its Responsibilities and Collaborators. In general, while state-based testing is great for defining Responsibilities, mocks give me the ability to precisely describe the Collaborators in my tests. Since I’m all in for using tests as executable documentation, mocks turned out to be a great technique to express my intent. Leading to my last point…

Mocking methods vs. mocking objects

My last point is not exactly an advantage of using mocks, but an example of how a framework can influence how you approach development. Coming from the Java world, I was used to encapsulate behaviour behind well defined interfaces. Now that I spend most of my time developing in Ruby, I not only have the ability of creating a mock/stub object, but also of mocking just one or two methods in a real object. These are usually called Partial Mocks.

Partial mocks allow me to express intent more precisely: instead of having to create a mock from an interface/class and record expectations in a proxy, I can specify the interactions in the real collaborators. It also makes the separation between state-based and interaction-based tests more loose, because I can selectively choose which methods will be mocked and which will be real. Finally, I can use tools such as Synthesis to verify that the interactions I’m mocking in one test are actually being verified in the collaborator’s test.

I know some people don’t feel comfortable using mocks and some even find it complex, but I think there’s great value on using it in the appropriate context. Instead of complaining about different framework’s idiosyncrasies and complexities, I think people should focus on its benefits as a design technique.

Post to Twitter

Programadores não treinam. Essa é uma triste constatação para a grande maioria dos programadores. Apesar de não ser verdade para todos os casos, aprendi com o Scott Adams que é importante começar com uma boa frase de impacto. Agora que tenho a sua atenção, posso falar da minha tentativa de mudar essa realidade: O Dojo@SP.

O que é o Dojo?

O Dojo é um espaço onde programadores se reúnem para treinar e aprender. As reuniões são periódicas e centradas num desafio de programação. Apesar do desafio, o objetivo não é terminar o problema. A idéia é aprender com as experiências vivenciadas pelo grupo. O ambiente é inclusivo, seguro e convidativo.

Dojo Audience

Eu vejo vários princípios de XP na forma como o Dojo funciona: passos de bebê, humanidade, falha, redundância, qualidade, melhoria, dentre outros.

Randori

As reuniões geralmente são conduzidas em dois formatos: no formato Kata alguém resolve o desafio em casa e apresenta na reunião “ao vivo”, começando do zero e seguindo TDD. No formato Randori o problema é resolvido “ao vivo” pelos participantes, usando TDD e Programação Pareada em turnos. A cada turno, o piloto volta para a platéia, o co-piloto passa a pilotar e um novo co-piloto é convidado da platéia.

Kata

Um pouco de história

A idéia do Kata como exercí­cio de treinamento foi proposta originalmente por Dave Thomas numa série de posts do seu blog. No final de 2003, Laurent Bossavit levou a analogia um passo adiante e propôs a criação de um espaço de treinamento em grupo um Dojo. A partir daí­, juntamente com Emannuel Gaillot, eles fundaram o Dojo de Paris, que está em funcionamento desde 2003. Outros gostaram da idéia e começaram movimentos semelhantes em outras partes do mundo.

Ruby Code

Depois de conhecer o Emannuel e a Emily no XP 2007, me motivei para começar um Dojo por aqui. Com o apoio deles e do wiki internacional, criado justamente para esse propósito, reuni alguns colegas e começamos a nos reunir no mês de Julho de 2007. Desde entnao estamos nos reunindo toda quarta-feira no Instituto de Matemática e Estatí­stica da USP.

Retrospectives

No Brasil, o Ivan Sanchez foi o primeiro a trazer essa idéia, fundando o DojoFloripa. Nossa iniciativa é a segunda do Brasil e esperamos que a participação do público paulista cresca no futuro e que outros Dojos comecem a aparecer em outras localidades do país.

Update: Esqueci de mencionar o Dojo de Recife que começou suas atividades no mesmo mês que a gente.

Como posso participar?

Se você está em São Paulo, junte-se a nós! Leia um pouco mais sobre nossas experiências no wiki e entre no nosso grupo de discussão. Não é necessário ter profundos conhecimentos de Python, Ruby ou TDD. Caso não esteja seguro para programar “ao vivo”, participe como ouvinte para sentir o clima. Espero vocês lá!

Food for the

Boa programação!

Post to Twitter

Esse post de propaganda vai ser curto. Tenho duas novidades para compartilhar com vocês.

Treinamento de TDD na PyCon Brasil

Eu e o RBP vamos ministrar um treinamento de TDD na Pycon Brasil 3, em Joinville, SC. O treinamento será no dia 31 de Agosto (última sexta-feira do mês) das 9:00hs ao 12:30hs. Maiores informações podem ser obtidas no site do evento. Espero encontrá-los na conferência!

AgilVídeo

A AgilCoop está lançando uma nova série de vídeos das nossas palestras e treinamentos: o AgilVídeo. Já temos 7 episódios publicados de alguns de nossos cursos de verão do começo do ano. Mais vídeos estão em edição (valeu, Julian!) e devem aparecer por lá em breve! Por isso, assinem nosso novo RSS para acompanhar as novidades. Além disso, quem ainda não conhece pode ouvir também nossa série de podcasts: o AgilCast. Novos episódios também devem sair em breve. Aguardem!

Post to Twitter

June 18th, 2007XP 2007 – Dia 1

Apesar de ler e concordar com o Thiago que esse tipo de post não é o ideal, meu blog se encaixa nessa categoria a maioria das vezes, portanto por que não continuar mais um pouco? :-) Prometo tentar melhorar depois dessa série. Além disso, sei que algumas pessoas vão se beneficiar desses relatos (me incluo nessa lista, pois esse tipo de registro já me foi útil em outras ocasiões e agora eu terei que escrever um relatório de qualquer jeito). Então, aproveitem os meus comentários sobre a XP 2007, diretamente de Como, na Itália.

Selling: How to Effectively Communicate the Value of Agility? – Joshua Kerievsky

Quem nunca teve problemas “vendendo” ou apresentando algumas das práticas ágeis para outras pessoas? Na AgilCoop eu me vi nessa situação diversas vezes: “Programação Pareada? Metade da Produtividade?”, “Parar para escrever testes e refatorar? Não posso parar, preciso entregar novas funcionalidades!”, e por aí vai. Por causa dessas e outras pré-concepções (não estou dizendo preconceito, muitas vezes eles são infudados), muitas pessoas resistem a mudanças.

No tutorial, Joshua, fundador da Industrial Logic e escritor do ótimo livro Refactoring to Patterns (que, por sinal, estou lendo agora), nos fez passar pela experiência da mudança através de uma pequena simulação, mostrando como mudar é anti-natural para as pessoas e como geralmente associamos mudança com a perda de alguma coisa (ao invés de ganho). Uma das discussões foi sobre a curva de mudanças de Virginia Satir, que diz que toda mudança leva um tempo para surtir efeito, passando por uma fase de caos antes de atingir um novo patamar, melhor que o anterior:

Virginia Satir Change Curve

Através de algumas histórias sobre vendas bem e mal sucedidas, ele apresentou algumas das características de uma boa venda:

  • Venda apenas o necessário: Isso inclui conhecer muito bem o problema e o contexto do seu cliente. Muitas vezes a solução para um problema pode ser obtida com custo reduzido se o verdadeiro problema é identificado. Quando a única ferramenta disponível é um martelo, todos os problemas se parecem com um prego.
  • Venda riscos: ao invés de enfatizar os benefícios de uma prática ou outra, discuta com os interessados quais os riscos associados a não implementá-las.
  • Compartilhe experiências: um gerente ou executivo não vai entender os benefícios dos testes automatizados, pois essa não é a linguagem que ele entende. Procure compartilhar vídeos, fotos e experiências de equipes que obtiveram sucesso, para que ele vivencie os benefícios.

Diversos outros assuntos e histórias foram compartilhados nesse tutorial, além de algumas boas recomendações de leitura: Selling the Invisible e The Secrets of Consultancy (esse eu já ouvi falar muito bem em diversos outros lugares).

Coder's Dojo: Acceptance-Test Driven Development in Python – Emily Bache and Geoff Bache

Nessa apresentação, em formato de Dojo, resolvemos parte de um problema do Ruby Quiz (Texas Hold'em) em Python, usando uma variação um pouco diferente de TDD, escrevendo os testes de aceitação antes. Nesse caso, o teste de aceitação final seria a saída esperada do programa, conforme a página de descrição no Ruby Quiz. Ao partir de cenários mais simplificados (porém ainda de aceitação) e utilizando o TextTest como ferramenta de teste, resolvemos parte do problema (infelizmente, o problema era grande demais para o tempo disponível). Algumas das minhas impressões da experiência:

  • Como o exercício era relativamente simples comparado a um cenário real, os testes de aceitação, que escrevíamos incrementalmente, eram muito parecidos com testes de unidade. Apesar disso, os exemplos ainda estavam na “linguagem de negócio” do poker. Isso pode ter confundido um pouco algumas pessoas da platéia que não tinham experiência com TDD.
  • Para quem tem experiência com TDD os passos pareciam um pouco grandes demais, apesar de ainda pequenos. Muitas vezes precisamos implementar muita coisa para fazer um teste de aceitação passar. Uma discussão interessante que tive depois da sessão, com o Emmanuel Gallot (fundador do Dojo de Paris, o mais antigo Dojo em funcionamento) e os apresentadores, percebemos que existem duas abordagens para aplicar TDD: enquanto alguns preferem escrever diversos testes de aceitação para “pedir” por uma implementação, outros preferem quebrar as necessidades que vão surgindo ao longo do caminho, e aplicar TDD separadamente para cada parte antes de integrar tudo no cenário de aceitação inicial. A primeira abordagem tem a vantagem de conectar toda funcionalidade com um teste de aceitação, enquanto a segunda acaba gerando diversos passos intermediários que, em muitas situações, são mais fáceis de acompanhar e fazem o design surgir da necessidade. Eu me identifiquei mais com a segunda abordagem, apesar de entender que é possível seguir outro caminho.
  • O TextTest é uma ferramenta de teste em Python baseada na comparação de texto ou, em outras palavras, um teste baseado em diffs da saída do programa. Eu não conhecia a ferramenta e achei interessante duas práticas aplicadas pelos apresentadores (que estavam seguindo a primeira das abordagens que apresentei acima): em primeiro lugar, a ferramenta permite salvar a saída esperada de acordo com o que aconteceu (resultado da execução do teste). Isso permite que você diga que o resultado do teste que acabou de falhar, na verdade está correto. A princípio, isso me pareceu “roubar”, mas como a abordagem deles era diferente, permitiu que eles convivessem com a implementação atual, podendo refatorar no verde (no final da sessão, algumas pessoas disseram preferir uma cor amarela para essa situação. Eu concordo). A segunda prática é a utilização de logs nos cenários de teste. Como a ferramenta se baseia na saída de texto do programa, os apresentadores puderam testar passos intermediários com logs que “explicam” o que o programa está fazendo. Eu pessoalmente batizei a técnica de printf-debugging automatizado :-)

Bom, por hoje foi isso. Continuo com os relatos com o passar da semana. Se estiverem interessados em fazer alguma pergunta para alguém importante aqui na conferência, deixem um comentário que eu tentarei entrar em contato. Alguns dos nomes que podem interessar estão disponíveis no website da Conferência.

Post to Twitter

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

Post to Twitter

September 21st, 2006Palestra sobre TDD

Um pouco em cima da hora, mas apresentarei um workshop sobre Desenvolvimento Dirigido por Testes no próximo sábado.



Enquanto os novos posts da série “TDD em Combate” ainda não saem (eu sei, já faz tempo que estou devendo, mas ele ainda não morreu. Já temos mais material para o próximo post, só falta escrever :-)), apresentarei junto com o RBP um workshop sobre Desenvolvimento Dirigido por Testes. O evento acontecerá no SENAC Santo Amaro e será uma demonstração ao vivo dessa técnica no desenvolvimento de um pequeno jogo.

Mais informações no portal da AgilCoop e no site do evento.

Post to Twitter

É 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 BDUFBig 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.TestCase):
    def testHigherRankWins(self):
        assert Piece("sergeant").attack(Piece("soldier")) > 0

Rapidamente, fazemos o teste passar:

# Stratego.py
 
class Piece:
    # (...) Constructor
    def attack(self, defender):        return 1    # (...) Exceptions

Novamente, apesar desse código não ser o que desejamos, é o suficiente para fazer o teste passar e para permitir que possamos implementar o próximo teste:

# StrategoTest.py
 
class AttackTest(unittest.TestCase):
    def testHigherRankWins(self):
        assert Piece("sergeant").attack(Piece("soldier")) > 0
    def testLowerRankLoses(self):        assert Piece("miner").attack(Piece("colonel")) < 0

Mais uma vez, implementamos o mais simples para fazer o teste passar:

# Stratego.py
 
class Piece:
    # (...) Constructor
    def attack(self, defender):        return self.rank - defender.rank    # (...) Exceptions

O teste para verificar uma situação de empate já funciona sem nenhuma mudança na implementação, assim como o teste para confirmar que o marechal vence o espião quando o ataca. Isso pode acontecer às vezes, permitindo que passemos para o próximo teste que deixará a barra de testes vermelha. Isso acontece com o teste que verifica se o espião vence um ataque contra o marechal:

# StrategoTest.py
 
class AttackTest(unittest.TestCase):
    def testHigherRankWins(self):
        assert Piece("sergeant").attack(Piece("soldier")) > 0
    def testLowerRankLoses(self):
        assert Piece("miner").attack(Piece("colonel")) < 0
    def testTie(self):        assert Piece("major").attack(Piece("major")) == 0    def testMarshalWinsSpy(self):        assert Piece("marshal").attack(Piece("spy")) > 0    def testSpyWinsMarshal(self):        assert Piece("spy").attack(Piece("marshal")) > 0

Uma simples validação do caso excepcional, faz com que os testes funcionem novamente:

# Stratego.py
 
class Piece:
    # (...) Constructor
    def attack(self, defender):        if self.rank == 1 and defender.rank == 10:            return 1        else:            return self.rank - defender.rank    # (...) Exceptions

O próximo caso excepcional acontece quando o cabo-armeiro ataca uma bomba:

# StrategoTest.py
 
class AttackTest(unittest.TestCase):
    def testHigherRankWins(self):
        assert Piece("sergeant").attack(Piece("soldier")) > 0
    def testLowerRankLoses(self):
        assert Piece("miner").attack(Piece("colonel")) < 0
    def testTie(self):
        assert Piece("major").attack(Piece("major")) == 0
    def testSpyWinsMarshal(self):
        assert Piece("spy").attack(Piece("marshal")) > 0
    def testMarshalWinsSpy(self):
        assert Piece("marshal").attack(Piece("spy")) > 0
    def testMinerWinsBomb(self):        assert Piece("miner").attack(Piece("bomb")) > 0

O necessário para fazê-lo passar é adicionar mais uma validação para o novo caso excepcional:

# Stratego.py
 
class Piece:
    # (...) Constructor
    def attack(self, defender):        if (self.rank == 1 and defender.rank == 10) or (self.rank == 3 and defender.rank == 11):            return 1        else:            return self.rank - defender.rank    # (...) Exceptions

Com a barra verde, podemos refatorar o código. Nesse caso, podemos eliminar a duplicação na verificação dos casos excepcionais criando uma lista dos pares que vencem quando atacam (apesar de possuírem um rank menor). O código refatorado fica assim:

# 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}
    __winsAttacking = [(1,10), (3,11)]    # (...) Constructor
    def attack(self, defender):
        if (self.rank,defender.rank) in self.__winsAttacking:            return 1
        else:
            return self.rank - defender.rank
    # (...) Exceptions

Para terminarmos nossa implementação, só falta lidar os casos de ataque inválido. Primeiro vamos escrever um teste para garantir que uma bomba não pode ter iniciativa de ataque:

# StrategoTest.py
 
class AttackTest(unittest.TestCase):
    def testHigherRankWins(self):
        assert Piece("sergeant").attack(Piece("soldier")) > 0
    def testLowerRankLoses(self):
        assert Piece("miner").attack(Piece("colonel")) < 0
    def testTie(self):
        assert Piece("major").attack(Piece("major")) == 0
    def testSpyWinsMarshal(self):
        assert Piece("spy").attack(Piece("marshal")) > 0
    def testMarshalWinsSpy(self):
        assert Piece("marshal").attack(Piece("spy")) > 0
    def testMinerWinsBomb(self):
        assert Piece("miner").attack(Piece("bomb")) > 0
    def testBombCannotAtack(self):        try:            Piece("bomb").attack(Piece("miner"))        except InvalidAttack:            pass        else:            fail("expected InvalidAttack exception")

E o código para fazer o teste passar é uma nova validação no método attack():

# Stratego.py
 
class Piece:
    # (...) Constructor
    def attack(self, defender):
        if self.rank == 11:            raise InvalidAttack()        if (self.rank,defender.rank) in self.__winsAttacking:
            return 1
        else:
            return self.rank - defender.rank
    # (...) Exceptions
 
class InvalidAttack(Exception):
    pass

O último teste é para evitar que uma bandeira tenha iniciativa de ataque:

# StrategoTest.py
 
class AttackTest(unittest.TestCase):
    def testHigherRankWins(self):
        assert Piece("sergeant").attack(Piece("soldier")) > 0
    def testLowerRankLoses(self):
        assert Piece("miner").attack(Piece("colonel")) < 0
    def testTie(self):
        assert Piece("major").attack(Piece("major")) == 0
    def testSpyWinsMarshal(self):
        assert Piece("spy").attack(Piece("marshal")) > 0
    def testMarshalWinsSpy(self):
        assert Piece("marshal").attack(Piece("spy")) > 0
    def testMinerWinsBomb(self):
        assert Piece("miner").attack(Piece("bomb")) > 0
    def testBombCannotAtack(self):
        try:
            Piece("bomb").attack(Piece("miner"))
        except InvalidAttack:
            pass
        else:
            fail("expected InvalidAttack exception")
    def testFlagCannotAtack(self):        try:            Piece("flag").attack(Piece("lieutenant"))        except InvalidAttack:            pass        else:            fail("expected InvalidAttack exception")

Podemos adicionar uma nova validação para evitar o ataque de uma bandeira:

# Stratego.py
 
class Piece:
    # (...) Constructor
    def attack(self, defender):
        if self.rank == 11 or self.rank == 0:            raise InvalidAttack()        if (self.rank,defender.rank) in self.__winsAttacking:
            return 1
        else:
            return self.rank - defender.rank
    # (...) Exceptions

Para finalizar, podemos aproveitar a barra verde para remover uma última duplicação, na validação das peças que não podem atacar. Após a refatoração, o código completo e final fica assim:

# 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")
 
class AttackTest(unittest.TestCase):
    def testHigherRankWins(self):
        assert Piece("sergeant").attack(Piece("soldier")) > 0
    def testLowerRankLoses(self):
        assert Piece("miner").attack(Piece("colonel")) < 0
    def testTie(self):
        assert Piece("major").attack(Piece("major")) == 0
    def testSpyWinsMarshal(self):
        assert Piece("spy").attack(Piece("marshal")) > 0
    def testMarshalWinsSpy(self):
        assert Piece("marshal").attack(Piece("spy")) > 0
    def testMinerWinsBomb(self):
        assert Piece("miner").attack(Piece("bomb")) > 0
    def testBombCannotAtack(self):
        try:
            Piece("bomb").attack(Piece("miner"))
        except InvalidAttack:
            pass
        else:
            fail("expected InvalidAttack exception")
    def testFlagCannotAtack(self):
        try:
            Piece("flag").attack(Piece("lieutenant"))
        except InvalidAttack:
            pass
        else:
            fail("expected InvalidAttack exception")
 
suite = unittest.TestSuite((unittest.makeSuite(CreatePieceTest),
                            unittest.makeSuite(AttackTest)))
 
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}
    __winsAttacking = [(1,10), (3,11)]
    __cannotAttack = [0,11] 
    def __init__(self, name):
        try:
            self.rank = self.__pieces[name]
        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
 
class InvalidPiece(Exception):
    pass
 
class InvalidAttack(Exception):
    pass

Conclusões e próximos passos

Terminamos o segundo post com 70 linhas de código de teste e 26 linhas de código de produção. Como vocês puderam notar, o ritmo imposto pelo TDD é baseado em passos pequenos e resultou num código simples e comunicativo. Por enquanto, temos apenas uma classe para representar uma peça do jogo. No próximo post implementaremos as regras de movimentação no tabuleiro, não percam!

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.

Post to Twitter


© 2007-2009 Danilo Sato | Powered by Wordpress

Page optimized by WP Minify WordPress Plugin