Design Ágil com Extreme Programming

Veremos o fluxo de um projeto que adota XP e o que pode ser utilizado na definição da arquitetura do software, apresentando a visão que um arquiteto deve ter nas fases de um projeto ágil e as práticas que dão suporte na construção de um Design evolutivo.

Por que eu devo ler este artigo:

Serve para arquitetos, analistas e desenvolvedores que desejam conhecer os valores por trás das metodologias ágeis e como funciona o Processo da Extreme Programming, uma das mais difundidas metodologias ágeis.

Veremos o fluxo de um projeto que adota XP e o que pode ser utilizado na definição da arquitetura do software, apresentando a visão que um arquiteto deve ter nas fases de um projeto ágil e as práticas que dão suporte na construção de um Design evolutivo. Por fim, é apresentada a prática de Desenvolvimento Dirigido a Testes, também conhecida como TDD, em conjunto com testes de Aceitação, que é utilizada para auxiliar o usuário a definir os critérios de aceitação durante a criação de suas estórias.

O tema é útil para toda empresa ou time de desenvolvimento que quer implantar metodologias ágeis – como Extreme Programming – e/ou quer aplicar o uso de TDD auxiliado por testes de aceitação. O tema é importante também para arquitetos que desejam saber como lidar com questões de arquitetura de software em um processo XP e quais as práticas que auxiliam nestas questões.

A terceira e última parte deste artigo apresenta os valores do Manifesto Ágil e sua influência em métodos como Scrum e XP. Abordamos também como a Arquitetura de Software se enquadra no processo ágil e quais as práticas de Extreme Programming que auxiliam os desenvolvedores e arquitetos de software a evoluir um sistema atendendo aos requisitos do cliente e boas práticas de Design. Para finalizar, o artigo apresenta também a implementação de uma funcionalidade para um sistema fictício utilizando a prática de Test Driven Development (TDD) e Acceptance Test Driven Development (ATDD).

Na primeira e segunda parte desta série de artigos, publicados nas Edições 80 e 81, abordamos sobre fundamentos de arquitetura de software, padrões de projetos em uma arquitetura distribuída, analisamos como identificar sintomas de um software mal planejado, e como refatorar a sua aplicação para utilizar boas práticas de desenvolvimento OO.

Na última parte desta série, vamos analisar a fundo como funciona um Projeto XP, passando por todas as suas fases, e veremos também como adotar as práticas de engenharia da Extreme Programming para construir um software aplicando as melhores práticas de desenvolvimento OO utilizando TDD e testes de aceitação.

Abordaremos os valores destas metodologias através do Manifesto Ágil, apresentando de maneira clara como estes valores estão presentes na Extreme Programming. Analisaremos também, como a Arquitetura do Software é tratada em um projeto ágil e como algumas práticas do XP auxiliam na criação e evolução da Arquitetura. Por fim, vamos apresentar um estudo de caso onde implementaremos uma funcionalidade em um sistema fictício, mostrando desde a escrita da estória pelo cliente até sua implementação utilizando testes de aceite com o framework FitNesse e TDD.

O Manifesto Ágil

Nos últimos 10 anos vem emergindo um movimento que promove uma nova maneira de se desenvolver software, conhecido como Agile.

Este movimento tem ganhado muita força e notoriedade graças a Agile Alliance, uma organização sem fins lucrativos, formada por grandes nomes da comunidade de desenvolvimento de software, como Kent Beck, Alistair Cockburn, Ward Cunningham, Martin Fowler, Robert C. Martin, Ken Schwaber, entre outros.

Em 2001, estes profissionais, criadores de metodologias como Extreme Programming, Scrum e Feature Driven Development, se reuniram em um resort de esqui em Utah nos Estados Unidos, para discutir novas maneiras de se produzir software, de um modo menos burocrático, mais simples e focado em pessoas. Foi nesta reunião que nasceu o termo Desenvolvimento de Software Ágil, e mais importante, foi onde nasceu o Manifesto Ágil.

O Manifesto Ágil é uma declaração dos princípios que fundamentam o desenvolvimento ágil de software, expressos por meio de quatro valores fundamentais:

É importante frisar que o manifesto ágil não exclui em suas práticas o uso de ferramentas e outros elementos oriundos do desenvolvimento de software tradicional, mas que, dentro de uma escala, os valores que estão em negrito nos princípios acima, são mais importantes do que os valores que estão à direita.

O manifesto contempla ainda 12 princípios (veja o site na seção Links) que endossam seu conteúdo e que servem como fundamento para toda metodologia considerada ágil.

Extreme Programming

Extreme Programming (ou simplesmente XP) é uma metodologia ágil para desenvolvimento de software proposta por Kent Beck, ideal para times pequenos e médios, que lidam com projetos complexos, onde os requisitos são vagos ou mudam constantemente.

Além de ser um método prescritivo simples de desenvolvimento, com diversas práticas de engenharia, de acordo com Kent Beck, XP é uma filosofia de desenvolvimento de software, pois integra não só os valores expressos no manifesto ágil, mas também propõe valores como Comunicação, Feedback, Simplicidade, Coragem e Respeito.

O ciclo de um projeto XP é apresentado na Figura 1.

Figura 1. Fluxo de um Projeto XP (Adaptado de Don Wells).

Uma das premissas em um projeto XP é a participação efetiva do cliente junto à equipe de desenvolvimento, para que ambos possam trabalhar em conjunto com o objetivo de produzir um software que traga o máximo de valor para a empresa. Dentro deste contexto, no fluxo apresentado na Figura 1, podemos observar que um projeto XP inicia na Fase de Exploração, onde o cliente transmite a necessidade do projeto para o time de desenvolvimento, relatando a importância do mesmo para o negócio.

Nesta fase, é elaborada apenas uma projeção inicial da solução, sem focar nos detalhes de implementação, para que o time de desenvolvimento possa explorar soluções em potencial, reduzir o risco de problemas técnicos e aumentar a confiabilidade das estimativas das estórias do usuário (User Stories), o que auxilia o cliente a decidir se continua ou não com o projeto.

Conforme veremos ainda neste artigo, uma Estória do Usuário é a maneira como são documentados os requisitos do cliente, similar a um caso de uso com menos detalhes e de forma mais sucinta. Elas são estórias contadas pelo próprio usuário, onde são descritas as funcionalidades que ele quer que o sistema realize. Como complemento, em um projeto XP o usuário também descreve os critérios de aceite e as restrições de uma estória, que servirão posteriormente para auxiliar na criação dos testes de aceitação e na validação do que foi desenvolvido pelo time.

Com uma projeção da arquitetura em mãos, o projeto entra na Fase de Planejamento. No entanto, na transição entre estas etapas, baseado nas estórias que devem ser implementadas, o time de desenvolvimento pode criar uma metáfora para o sistema (System Metaphor). Esse sistema de metáfora é a maneira como o time dissemina o conhecimento sobre o sistema, utilizando uma linguagem comum que transmita a arquitetura adotada no projeto, de modo que qualquer membro consiga facilmente explicar o Design do mesmo para novas pessoas sem precisar de uma pilha de documentos. Em times que aplicam DDD (Domain Driven Design) uma boa metáfora de sistema serve como base para a criação da Linguagem Onipresente (Ubiquitous Language).

Após a criação da metáfora, o próximo passo é fazer o planejamento da release (Release Planning), que é realizado por meio de uma ou mais reuniões com todos os envolvidos no projeto, contando inclusive com a participação dos responsáveis pelo negócio. Durante as reuniões, as estimativas são feitas por uma prática conhecida como “Jogo do Planejamento”.

Nesta prática, é responsabilidade do time de desenvolvimento fazer uma estimativa (em pontos) de cada uma das estórias do usuário, contemplando a complexidade e o prazo de entrega. Para uma estimativa mais assertiva, também é importante que durante o jogo os envolvidos comuniquem os impactos técnicos de implementação dos requisitos. E por fim, criar as tarefas (técnicas) necessárias para atender a estória, como por exemplo, a criação de objetos no banco de dados, criação de web services, tarefas de teste, ou seja, todo trabalho técnico necessário para concluir a implementação da estória.

Caso haja questionamentos por parte do time, o cliente responsável deve estar presente para tirar qualquer dúvida referente à estória, priorizá-las de acordo com sua criticidade e importância para o negócio, e auxiliar o time a definir as datas para a release.

Ao final da(s) reunião(ões), o time terá em mãos o plano de release com os itens de menor e maior prioridade devidamente estimados, e com prazos acordados para a entrega da release. Este artefato será utilizado para planejar cada iteração, que deve durar de uma a três semanas. É importante frisar que este planejamento está sempre passando por modificações, como o próprio manifesto ágil ilustra, onde um de seus valores nos diz que responder às mudanças é mais importante do que seguir um plano (um cronograma, por exemplo), para melhor atender ao cliente.

Em seguida é feito uma breve reunião no início de cada iteração para detalhar as tarefas atribuídas ao time. Pode entrar nesta lista qualquer tarefa a ser desenvolvida, como as estórias de maior prioridade, bugs a serem resolvidos ou tarefas que não foram aprovadas em iterações anteriores, ficando esta decisão a critério do time e do cliente.

As estórias que devem entrar em cada iteração são escolhidas respeitando a velocidade de desenvolvimento do time (timebox). Essa velocidade é baseada na soma dos pontos de todas as estórias que o time conseguiu entregar na última iteração.

Uma vez iniciada a iteração, para promover e maximizar a comunicação entre os membros do time, é realizado diariamente uma Stand Up Meeting, uma reunião simples, de no máximo 15 minutos, cujo objetivo é fazer com que o time exponha os problemas, soluções e o trabalho realizado até o momento. Mediada por um dos membros, nesta reunião, como o próprio nome indica, todos ficam em pé e focam apenas no status report do projeto.

Ao final de cada iteração, se os testes de aceite (Acceptance Tests) foram realizados com sucesso e o usuário responsável pela estória aprovou o desenvolvimento, então é feito um pequeno release (Small Release) das funcionalidades desenvolvidas, integrando-as ao sistema (ou produto).

Durante o trabalho, em XP a programação é feita em pares, ou seja, dois desenvolvedores em conjunto tentam resolver o mesmo problema; enquanto um digita o outro fica auxiliando, procurando por bugs e sugerindo melhorias. Antes de um par começar a desenvolver, é feito uma negociação de quem será os pares do dia, ou seja, diariamente é feito um rodízio, pois assim o conhecimento não fica segmentado, evitando as Ilhas de Conhecimento. Além disso, a programação em pares melhora a comunicação e o feedback entre o time, e inibe a falta de foco no trabalho.

Durante o desenvolvimento, os pares realizam o trabalho de codificação e testes aplicando a técnica Test Driven Development, ou simplesmente TDD, conforme veremos mais adiante.

User Stories

Uma user story, ou estória do cliente, é uma estória que descreve uma funcionalidade desejada pelo usuário de um software e que pode trazer valor para seu negócio. William C. Wake, autor do livro Extreme Programming Explored, sugere que para se criar uma boa user story, o responsável deve focar em seis atributos, cujas letras iniciais formam o acrônimo INVEST, e que significam:

Uma boa prática ao escrever user stories, é aplicar um template que facilite a escrita da estória para o usuário e o entendimento da mesma pelo time. Em projetos ágeis, as equipes geralmente utilizam o seguinte template:

Como <tipo de usuário / papel>, eu posso <objetivo/função> para <razão/valor de negócio>.

Neste formato incluímos poucas, mas importantes informações que facilitam a compreensão de uma estória. Para facilitar o entendimento, apresentamos abaixo alguns exemplos para um sistema fictício de reservas de hotel.

Como usuário, eu posso reservar um quarto de hotel. Como usuário, eu posso cancelar uma reserva. Como usuário, eu posso fazer buscas de hotéis. Como usuário, eu posso filtrar a buscas de hotéis para visualizar apenas os hotéis que quero. Como administrador, eu posso credenciar novos hotéis para aumentar as opções de escolha do cliente.

Nestes pequenos exemplos, conseguimos identificar o papel do usuário na história, a funcionalidade ou ação que o mesmo deseja executar no sistema e, opcionalmente, podemos descrever o valor que a estória representa para o negócio.

Em um projeto XP, como já informado, é aconselhável que o cliente escreva as estórias e os critérios de aceitação, pois assim seu envolvimento e comprometimento aumentam naturalmente.

Porém, é ideal que na estória não haja detalhes de interface gráfica. O que pode ocorrer durante a escrita das estórias é a criação de um protótipo de tela em um documento separado, que ajude o cliente a transmitir melhor a ideia de como deve ser o resultado final. E por fim, vincular o protótipo e os testes de aceite à estória. Veja um exemplo na Figura 2.

Figura 2. User Story com critérios de aceite e protótipo de tela.

Arquitetura de Software em Processos Ágeis

Existem várias definições para Arquitetura de Software. De acordo com a Wikipédia, a arquitetura de software de um sistema consiste nos componentes de software, suas propriedades externas, e seus relacionamentos com outros softwares.

Em metodologias de software tradicionais, como a Waterfall, geralmente a Arquitetura do Sistema é definida antes de iniciar a construção do software. O lado negativo desta abordagem é que uma vez que toda a arquitetura e o escopo do sistema tenham sido definidos, após a entrega do Software, qualquer mudança tem um impacto muito maior no Design da aplicação, pois a validação do cliente é feito somente na entrega, dificultando sua mudança.

Em Extreme Programming, esse impacto é menor, pois com iterações de duas a três semanas, o feedback do cliente é muito maior que em um projeto tradicional. É o que ilustra o gráfico da Figura 3.

Figura 3. Custo de mudança é menor em um projeto XP.

Em um projeto de software, para o usuário final e até para o cliente, não importa a arquitetura adotada, mas ela é extremamente importante para designers, desenvolvedores e testers que irão se beneficiar (ou não) com ela.

Em projetos ágeis, a arquitetura e o design da aplicação são feitos em diferentes momentos durante um projeto. Em cada um deles, o nível de detalhes é visto em horizontes diferentes, e em cada um destes horizontes, o time deve balancear o nível de detalhe de acordo com a fase do planejamento, conforme demonstra a metáfora apresentada na Figura 4.

Figura 4. Visão da Arquitetura em diferentes momentos.

Na Figura 4 criamos uma metáfora sobre a visão que um arquiteto deve ter em cada uma das fases do projeto, ilustrando através da visão de uma cidade observada por cima. Na visão de Longo Prazo, o horizonte que o time deve ter é de alto nível, pois neste momento, geralmente, é feito apenas uma trabalho de viabilidade ou definição da visão do projeto.

Na visão de Médio Prazo, o time já sabe o estilo de arquitetura adotado e alguns detalhes sobre o modelo de domínio, com informações suficientes para realizar o Planejamento da Release. E na visão de Curto Prazo, que acontece durante a iteração que o time está atuando, ou seja, durante o desenvolvimento, o time deve conhecer em detalhes como a estória será implementada, tendo total liberdade de decidir durante a codificação qual a melhor maneira de evoluir o Design da aplicação. Neste momento, o time já consegue ver detalhes da arquitetura e sabe o que deve ser feito.

Pelo fato de XP não dar tanta ênfase à arquitetura inicial do projeto, não significa que este assunto não seja tratado dentro da metodologia. XP atende à questão da arquitetura por meio de alguns mecanismos, como:

Spikes (Prototipação)

Como dissemos anteriormente, podemos criar protótipos ou provas de conceito (Spikes) na fase de análise inicial do projeto, ou durante o planejamento da release, que é o momento onde o time tem a oportunidade de discutir possíveis soluções para o problema. Em um ambiente XP, geralmente todo o time trabalha no mesmo local, próximos uns dos outros, com lousas na parede e post its ou flipcharts, de modo a facilitar a comunicação. Em um ambiente como esse, qualquer um dos membros consegue facilmente comunicar ou propor um modelo/solução para implementação de uma estória. Isso pode ser feito por meio de Spikes, para que todos os envolvidos possam discutir e chegar a um consenso em relação ao protótipo.

Um Spike pode ser também uma prova de conceito ou um experimento utilizando algum framework de teste (por exemplo, JUnit) para pesquisar rapidamente a resposta para um problema.

Sistema de Metáforas

Conforme dissemos no início do artigo, a metáfora criada pelo time guia o desenvolvimento do sistema, auxiliando a todos os envolvidos a entender os elementos básicos do projeto e seus relacionamentos.

A metáfora irá evoluir durante todo o ciclo do projeto, conforme o time aprende sobre o sistema. Este é um exemplo de como é flexível a forma que a arquitetura da aplicação é tratada no XP. Se a metáfora pode ser melhorada, então ela será alterada. Por exemplo: para explicarmos o conceito de Map do framework de Collections do Java, poderíamos criar uma metáfora dizendo que um Map é como um catálogo, onde cada item do índice nos leva ao objeto referenciado.

Primeira Iteração

As primeiras iterações são as mais importantes para a arquitetura. Em XP, a primeira iteração é também conhecida como Iteração Zero.

Nesta fase, baseado no conhecimento prévio sobre as funcionalidades e em uma estimativa inicial, o time tem o conhecimento necessário para criar um modelo inicial de arquitetura. O que traz alguns benefícios, como:

Algumas pessoas podem pensar que a iteração zero pode atrasar o início da codificação. Na verdade, este trabalho é feito justamente para potencializar a codificação nas iterações seguintes, e a arquitetura inicial é um estudo rápido feito pelo arquiteto em conjunto com o restante da equipe. Entre os artefatos que podem ser criados nesta fase, podemos citar os diagramas de infraestrutura, componentes, requisitos de performance, modelo de segurança, entre outros. Obviamente, respeitando sempre o manifesto ágil, documentando somente o que realmente precisamos.

Além disto, na Iteração Zero, é o momento que o time deve preparar o ambiente para o desenvolvimento e selecionar as ferramentas de suporte. Ao final desta iteração, o time de desenvolvimento deve estar pronto para desenvolver, fazer o build da aplicação, testar e rastrear o código.

Ainda nesta iteração, é importante o time considerar quais ferramentas e padrões de codificação serão utilizados no projeto, como por exemplo:

Com a Iteração Zero finalizada, o time começa a se planejar para a primeira iteração de desenvolvimento. A primeira iteração também é importante para a arquitetura, pois nesta fase o time deve selecionar um conjunto de estórias simples, que force a criação de pelo menos um esqueleto da arquitetura completa da aplicação. As estórias podem ser implementadas da maneira mais simples possível, o importante é que ao final da iteração o time tenha a base da arquitetura montada.

Integração Contínua

De acordo com Martin Fowler, Integração Contínua é uma prática de desenvolvimento de software onde os membros de uma equipe integram frequentemente o trabalho realizado, o que pode levar a múltiplas construções de todo o sistema durante o dia. Cada integração é verificada por um build automático (incluindo os testes) para detectar erros o quanto antes.

Esta abordagem ajuda a reduzir drasticamente problemas de integração, o que leva o time a identificar de forma rápida defeitos na aplicação e a produzir um software com mais qualidade. Existem várias ferramentas que auxiliam este processo, como Hudson, CruiseControl e Apache Continuum.

Geralmente um ambiente que utiliza Integração Contínua pode ser descrito através dos seguintes passos:

  1. O desenvolver efetua o processo de commit do novo código no sistema de controle de versão (ex.: Subversion, Git);
  2. Enquanto isso, o servidor de CI (Integração Contínua), periodicamente, analisa se o repositório de código foi alterado;
  3. Após o commit ocorrer, o servidor de CI faz o check-out do código alterado em um ambiente de testes;
  4. Em seguida, por meio do script de build, a ferramenta compila o código, faz o deploy na máquina de testes e executa os casos de teste existentes;
  5. Se os testes falharem, o sistema automaticamente efetua o rollback no sistema de controle de versão, para voltar para a última versão válida da aplicação (sem erros). E por último, envia uma notificação para o desenvolvedor e para o gerente relatando os problemas que ocorreram no processo de integração;
  6. (Opcional) Se os testes passarem, a ferramenta executa a inspeção e análise de cobertura. Se houver algum problema, é feito um reporte;
  7. Se os testes foram executados com sucesso, o desenvolvedor é informado que a implementação foi feita com sucesso.

A Figura Q1 apresenta uma visão gráfica deste cenário.

Figura Q1. Processo de Integração Contínua.

Pequenas Releases

Por meio de pequenas entregas, o “esqueleto” da arquitetura criado na primeira iteração é aos poucos preenchido, dando tempo para o time de desenvolvimento evoluir o sistema. As entregas são feitas de acordo com a prioridade definida pelo cliente.

Outro benefício importante das pequenas releases é que pelo fato das entregas serem homologadas pelo cliente, o feedback é muito maior, facilitando a identificação de áreas do sistemas que devem ser corrigidas.

Simple Design

Alguns críticos reclamam que utilizar XP é ruim para definição do Design, pois conforme foi explicado no início do artigo, não existe uma fase somente para Design, ou melhor, diferente do método Waterfall, não existe o conceito de Big Design Upfront.

Em XP, a busca por um design simples e limpo é levada ao extremo. A todo o momento os desenvolvedores buscam as soluções mais simples possíveis, o que significa que funções não solicitadas, algoritmos e estruturas mais complexas que o necessário são deixadas de lado. É fato que muitos desenvolvedores tentam “prever” o que os usuários vão precisar, e implementam estas funcionalidades no intuito de superar as expectativas do cliente. Em XP isto não acontece, pois a adição de funcionalidades não planejadas pode trazer riscos para o projeto, ou apenas não ser utilizado pelo cliente.

Para auxiliar a criação de um design simples, XP propõe o uso de quatro regras que ajudam o desenvolvedor a aplicar princípios como SRP, OCP e outros que apresentamos na segunda parte deste artigo. De acordo com XP, nosso código é considerado simples o suficiente quando apresenta as seguintes regras:

Para um sistema ser considerado testável, ele deve possuir uma cobertura de testes aceitável e passar em todos os testes toda vez que for executado, o que é óbvio, mas não deixa de ser uma regra muito importante, pois sistemas que não são testáveis não são verificáveis. E um sistema que não pode ser verificado, não pode ser implantado.

Tornar o sistema testável aplicando TDD (conforme veremos adiante), nos leva naturalmente a um Design onde as classes ficam pequenas e com um único propósito, respeitando o Princípio da Responsabilidade Única (SRP). Portanto, ter certeza de que nosso sistema é totalmente testável nos ajuda a criar um Design de melhor qualidade.

Um sistema com forte acoplamento entre seus componentes e objetos torna difícil a escrita de testes, pois a codificação não é realizada pensando em testes. Enquanto que ao desenvolvermos com testes, temos a chance de reduzir o acoplamento entre os objetos constantemente, visto que para cada classe/método é criado um teste unitário.

Dessa forma, com o desenvolvimento orientado a testes, temos mais oportunidades de aplicar princípios como o Princípio de Injeção de Dependência (DIP). Através dos testes controlamos a qualidade do código, uma vez que estamos sempre refatorando nossa aplicação.

Para as demais regras citadas anteriormente, para nosso código ser considerado simples (não conter código duplicado, possuir testes expressivos e um número mínimo de classes/métodos), utilizamos diversas refatorações durante o ciclo de desenvolvimento visando à melhora contínua do design e qualidade de nossa aplicação.

Uma vez com os testes criados, podemos melhorar ainda mais nossas classes, fazendo de maneira incremental refatorações em nosso código. Para cada linha de código que adicionamos, nós pausamos e refletimos sobre o design do código que acabamos de criar. Caso seja necessário, fazemos as alterações e em seguida rodamos os testes para ver se não afetamos outras áreas do sistema.

Um sistema bem testado transmite segurança para os desenvolvedores, para que os mesmos possam realizar as alterações no código sem medo de que aconteça algo de errado com a estrutura da aplicação.

Durante a refatoração, podemos aplicar todo nosso conhecimento sobre boas práticas de desenvolvimento e design de software que aprendemos na primeira e segunda parte deste artigo. Podemos aumentar a coesão, diminuir o acoplamento, separar as responsabilidades, modularizar o sistema, escolher nomes melhores e assim por diante. Através das refatorações conseguimos aplicar as últimas três regras para um design simples: eliminar duplicação (lembra-se do princípio DRY?), assegurar um código expressivo e minimizar o número de classes e métodos.

Além destes mecanismos, em XP a arquitetura da aplicação evolui de acordo com as iterações, pois durante as sessões de Pair Programming os desenvolvedores aplicam o desenvolvimento dirigido a teste.

TDD – Test Driven Development

Uma das práticas mais conhecidas em XP, o TDD é uma técnica de Design e desenvolvimento que auxilia a construção do software de maneira incremental, baseada em uma regra muito simples: Somente escreva código para ajustar um teste unitário que falhou. Em outras palavras, escreva o teste primeiro, e somente depois escreva o código que faça com que o teste passe, como pode ser visto no fluxo apresentado na Figura 5.

Figura 5. O Ciclo do TDD.

O processo é muito simples. Para utilizar TDD devemos aplicar os seguintes passos:

  1. Escrever um teste simples e criar uma asserção (assertion) que valide o teste. Ao executar a primeira vez, obviamente o teste deve falhar (vermelho);
  2. Implemente somente o código necessário para fazer com que os testes passem (verde). Nada mais do que isso, caso contrário o desenvolvedor perde o foco nos testes;
  3. Refatore o código sempre que necessário. Então comece o ciclo novamente.

Conforme desenvolvemos o sistema, utilizamos TDD para termos um feedback se o código que acabamos de criar funciona e se a qualidade do Design da aplicação está bem estruturada. Ao escrever os testes, fica claro para o desenvolvedor o critério de aceitação para a próxima sessão de testes, pois temos sempre que nos perguntar se o que fizemos é o suficiente. Com base na resposta, decidimos se devemos criar mais testes para evoluir o Design da aplicação.

Outro benefício é que a escrita dos testes nos encoraja a escrever componentes com baixo acoplamento. Quanto mais isolado o componente for, mais fácil será de testá-lo. Além disso, os testes servem como documentação, fornecendo uma descrição executável do que o código faz (Design), além de adicionar uma suíte de teste de regressão completa (implementação).

De acordo com Kent Beck, um dos criadores da XP, a melhor maneira de aprender TDD é utilizando a programação em pares, onde uma das pessoas “controla” o teclado e o mouse, pensando na melhor maneira de implementar o código, enquanto o outro auxilia na revisão e evolução do design, alternando as posições em períodos pré-determinados. Desta maneira, o feedback é maior e o conhecimento sobre o código não fica reservado a apenas um desenvolvedor.

Outro benefício é que durante a execução dos testes, o desenvolvedor consegue detectar erros de implementação enquanto o contexto da estória em desenvolvimento ainda está fresco em sua cabeça.

Conforme visto anteriormente, a regra de ouro do TDD nos diz que devemos apenas escrever um teste que falhe. Essa regra nos deixa algumas dúvidas ao iniciar o desenvolvimento, como por exemplo: qual o primeiro teste que devemos fazer? Como sabemos quando devemos parar de criar novos testes e evoluir nosso código?

Uma boa prática ao implementar uma nova funcionalidade é iniciar o ciclo de desenvolvimento escrevendo um teste de aceitação, o que nos ajuda a identificar e a focar na funcionalidade que queremos construir. Veja o fluxo na Figura 6.

O teste de aceitação serve de ponto de partida para o desenvolvimento. Enquanto ele está falhando, indica que o sistema ainda não implementou a funcionalidade. Ao trabalhar em uma funcionalidade, utilizamos os testes de aceitação (definidos pelo cliente, conforme veremos adiante) para nos guiar no código que devemos produzir, escrevendo somente o código que é relevante ao teste.

Figura 6. O Ciclo do TDD guiado por testes de aceitação.

Desta maneira, trabalhamos com dois níveis de teste, o Teste de Aceitação e o Suporte aos Desenvolvedores (Testes Unitários). Na Figura 6 o ciclo externo representa os testes de aceitação, que são testes que durante o ciclo de desenvolvimento demoram mais para passar, pois correspondem às funcionalidades que estão sendo desenvolvidas. Os testes internos dão suporte aos desenvolvedores, pois ajudam a manter a qualidade do código, evitando que novos bugs sejam adicionados. Uma boa prática é nunca salvar nos Sistemas de Controle de Versão código que possua testes unitários com falha. Deste modo o desenvolvedor pode perder um bom tempo até entender todo o contexto para o qual o teste foi criado.

Utilizando Objetos Mock para suporte aos Testes

Como vimos, uma vantagem do desenvolvimento dirigido a testes é que mesmo antes de começar a implementar o método, o desenvolvedor já tem definido qual será seu comportamento. Para tanto, o time tem que planejar no início do desenvolvimento como irá executar e automatizar os testes para cada uma das estórias.

Escrever código testável é um conceito simples, mas não é uma tarefa fácil, principalmente se estamos lidando com código legado, que não possui testes automatizados e que não foi projetado para ser testado. Sistemas legados geralmente possuem lógica de negócio, código de I/O e banco de dados misturados com código relacionado à camada de interface do usuário, o que dificulta muito a sua manutenção.

Uma abordagem comum ao criar um Design de uma arquitetura testável é separar e isolar as camadas que realizam diferentes funções na aplicação. O ideal é que você possa acessar cada camada diretamente com uma suíte de testes e algoritmos de teste com inputs diferentes. Conforme vimos na primeira parte deste artigo, em uma arquitetura em camadas você pode isolar a lógica de negócio na camada de domínio, o que facilita o uso de mocks para simular o acesso a aplicações dependentes ou até mesmo um banco de dados. Se a camada de apresentação pode ser separada das camadas de domínio e infraestrutura, você pode facilmente fazer vários testes de validação em cada uma delas de maneira isolada.

Quando estamos desenvolvendo um sistema orientado a objetos, ao analisarmos um diagrama de comunicação, o que vemos é a interação dos objetos e a troca de mensagens entre si, pois é assim que os objetos se comunicam, através de mensagens. Um objeto recebe uma mensagem de outro objeto, que reage enviando mensagens para outros objetos e talvez retornar um valor ou uma exceção para o objeto que iniciou a comunicação. Este conjunto de objetos, em tempo de execução, forma uma rede de objetos colaborativos, conforme ilustra a Figura 7.

Figura 7. Rede de objetos.

Esta abordagem nos permite alterar facilmente o comportamento do sistema, como mudar a composição dos objetos e utilizar combinações diferentes através de herança para formar novos objetos, ao invés de simplesmente escrevermos código procedural.

Quando estamos projetando os testes, ao modelar como os objetos podem enviar e receber as mensagens, o ideal é que o objeto que está chamando ou enviando uma mensagem descreva o que quer baseado no papel que o objeto destino (que está sendo chamado) exerce, e deixar o objeto chamado decidir a melhor maneira de executar a tarefa. Observe que estamos falando da aplicação do Princípio do Conhecimento Mínimo, apresentado no artigo anterior, onde o objeto chamador não pode saber como funciona a estrutura interna do objeto chamado. Na Listagem 1 apresentamos um exemplo de código que viola este princípio.

Listagem 1. Violação do Princípio do Conhecimento Mínimo.
painel.getComponent(AREA_DROP). getDropTarget(). getDropTargetContext(). dropComplete(Boolean.TRUE.booleanValue());

Após analisar e identificar a intenção do código, podemos refatorá-lo para que o mesmo fique igual à Listagem 2.

Listagem 2. Código limpo e expressivo, que revela a intenção.
painel.dropFinalizado();

Esta abordagem esconde todos os detalhes de implementação por trás da chamada do método. O desenvolvedor que for utilizar o objeto painel não precisa mais saber sobre a cadeia de métodos que ele precisa chamar para indicar ao componente que uma operação de drag-and-drop está completa. Outro princípio que acompanha muito bem este exemplo é o SRP (Princípio da Responsabilidade Única), pois assim conseguimos evitar que uma classe cresça desproporcionalmente, e assuma mais responsabilidades do que realmente possui.

Mas o que acontece quando queremos testar uma classe que depende de outros objetos? Como no caso da Figura 8, onde o círculo em destaque envia mensagens para pelo menos três objetos ao ser invocado.

Figura 8. Como testar um objeto dependente.

Para isso utilizamos os objetos mock, eles são perfeitamente adequados para testar um trecho de código de forma isolada do resto da aplicação. Os mocks substituem os objetos que os métodos que estão sob testes se relacionam. Uma das características de objetos mock é que eles não implementam nenhuma lógica, apenas uma “casca” que provê métodos que permitam aos testes controlar o comportamento de todos os métodos de negócio da classe que está sendo simulada (via mock).

A ideia é que o desenvolvedor consiga criar um objeto leve e controlável que substitua um objeto “real”, e tenha meios de determinar o comportamento esperado (valores de retorno), de acordo com os estímulos recebidos através de parâmetros. Existem várias bibliotecas Mock disponíveis para download. Entre as soluções open source mais populares podemos citar EasyMock, jMock e Mockito.

Na Figura 8, os três objetos com interrogação seriam executados por um objeto mock, com o objeto a ser testado isolado das demais dependências, sem precisar conhecer os detalhes da implementação. A Figura 9 apresenta este novo cenário.

Figura 9. Isolando um objeto para testes.

Utilizar objetos mock em nossos testes trás diversos benefícios, entre eles, podemos citar:

Agora que conhecemos os princípios utilizados em Extreme Programming para lidar com Arquitetura e Design, vamos analisar um estudo de caso.

Case: Implementando uma User Story com TDD e ATDD

Suponha que estejamos desenvolvendo um sistema de pagamento eletrônico chamado EasyPay, e no meio de uma das iterações, durante a reunião diária do projeto, um desenvolvedor optou por implementar a estória apresentada na Figura 10.

Figura 10. Estória selecionada para implementação.

A estória selecionada define claramente o que deve ser desenvolvido. Além disso, o usuário incluiu atrás do cartão os critérios de aceitação que devem ser avaliados para que a funcionalidade seja considerada finalizada. Observe que assim como a estória, os critérios de aceite são bem objetivos, pois omitem detalhes (de como estes critérios devem ser aplicados) que podem mudar até que chegue o momento de implementarmos a estória.

Com a estória em mãos, o próximo passo é escrever os testes baseado nos critérios de aceite definidos pelo cliente. Para tanto, o desenvolvedor se reúne com o cliente e começa a detalhar em uma lista os testes de aceite, adicionando mais detalhes e discutindo como a estória deve funcionar. Ao final da conversa, em conjunto com o cliente, o desenvolvedor chegou à seguinte lista de testes, apresentada na Figura 11.

Figura 11. Lista de Testes de Aceite.

Como os testes de aceitação têm o propósito de demonstrar para o cliente que a aplicação executa corretamente as funcionalidades solicitadas, o ideal é que o próprio cliente tenha a possibilidade de executar estes testes. Se observarmos a lista de testes da Figura 11, veremos que ela está em um formato aparentemente “executável”, pois separamos em três colunas qual é a ação, os parâmetros que a ação recebe, e por fim, o resultado esperado.

Para documentar e executar testes, criados a partir da perspectiva do negócio, existe o Framework FitNesse (veja o quadro “Introdução a FitNesse”). Nesta ferramenta, estas operações são feitas de forma amigável para o cliente. Por isso, utilizaremos o FitNesse para os testes de aceitação.

Do ponto de vista do desenvolvedor, estes testes de aceitação devem validar o sistema em um nível abaixo da camada de apresentação. Mais especificamente na camada de serviço (veja a Figura 12), pois ao escrever testes para esta camada, é possível testar quase todo o sistema e sua lógica de negócio, uma vez que não deve haver lógica de negócio na camada de apresentação.

Figura 12. Testes de aceitação acessam a camada de serviço.

Após documentar os cenários de teste junto aos usuários de negócio (veja a Figura Q2 do quadro “Introdução a FitNesse”), o time tem que implementar a estória atendendo aos critérios de aceitação que já foram documentados.

Neste momento, o time pode criar a classe Fixture – do FitNesse – para deixar pronto o cenário executável para os testes. Como estamos aplicando TDD, vamos criar a classe desenvolvendo apenas o suficiente para vincular os métodos de teste e seus atributos com os dados de nossa tabela Fit. O resultado deve ser similar à Listagem 3.

Listagem 3. Classe Fixture inicial para testes no FIT.
public class SMSServiceFit extends ColumnFixture{ public String celular; public String mensagem; public boolean valido(){ return true; } public String enviar(){ return ""; } public String getCelular() { return celular; } public void setCelular(String celular) { this.celular = celular; } public String getMensagem() { return mensagem; } public void setMensagem(String mensagem) { this.mensagem = mensagem; } }

Obviamente, esta versão ainda não é a final de nossa Fixture, mas neste momento isto ainda não importa. O que queremos é apenas executar os testes e receber o feedback (com erro). Para tanto, para que o FitNesse entenda que nossa classe é uma Fixture, estendemos a classe ColumnFixture, e para atender à primeira Fit Table, criamos o método valido() que retorna um valor booleano fixo (true) e o atributo celular para receber o valor da coluna de mesmo nome. Para a segunda Fit Table, além de celular criamos o atributo mensagem, que usaremos para validar o SMS, e o método enviar(), que neste momento retorna apenas um valor nulo.

Com a Fixture pronta, é preciso fazer o framework reconhecer a classe que acabamos de criar. Para tanto, precisamos apenas incluir o projeto no classpath por meio da própria página wiki, editando-a para referenciar o caminho do projeto que contém a Fixture. Assim, adicione o comando !path seguido do caminho para o projeto (veja na Figura Q2 do quadro “Introdução a FitNesse” o classpath informado). Com o ambiente configurado, basta pressionar o botão Test. Veja o resultado na Figura 13.

Figura 13. Resultado da execução dos testes com FitNesse.

Introdução a FitNesse

Existe uma categoria de ferramentas de teste, baseada em tabelas, que permite documentar e executar os nossos testes de uma maneira que tanto um humano quanto uma máquina possam entender. Uma das ferramentas mais famosas nessa categoria é o Framework for Integrated Test, ou simplesmente FIT, criado por Ward Cunningham, um dos signatários do Manifesto Ágil e criador/inventor do conceito de Wiki.

Uma boa extensão para o FIT é a ferramenta denominada FitNesse. Ela possui um formato de Wiki, o que facilita a criação de páginas e documentação dos testes. Na Figura Q2, vemos um exemplo de como ficaria nossa lista de testes de aceitação documentada em uma página Wiki nessa ferramenta.

Figura Q2. Documentando os testes com FitNesse.

Conforme demonstra a figura, documentamos nossos testes em tabelas chamadas Fit Tables, que servem tanto para a escrita dos testes quanto para a execução e apresentação dos seus resultados. O modelo tabular é utilizado para que os responsáveis pelo negócio auxiliem a equipe de desenvolvimento a criar os testes. Note que na primeira opção do menu à esquerda existe um botão chamado Test, que ao ser pressionado executa os testes documentados apresentando os resultados na própria página. Nesta página Wiki, somente as tabelas são executadas, o restante é ignorado pelo FIT.

Ainda na Figura Q2 visualizamos duas tabelas, ambas com cinco linhas. Na primeira linha das duas tabelas, onde está escrito com.netfeijao.easypay.service.SMSServiceFit, é onde informamos a nossa Fixture, ou seja, a classe que irá efetivamente executar os testes. Ela age como um componente intermediário entre a Fit Table e o sistema que estamos testando. Na segunda linha da primeira tabela os labels celular e valido? representam o atributo e o método da classe SMSServiceFit – o símbolo de interrogação “?” identifica o método). O restante das linhas representam os valores que serão utilizados no teste.

Deste modo, na hora de executar a tabela de testes, o framework cria um objeto da classe SMSServiceFit e passa o controle para este objeto. Ao final dos testes, de acordo com o retorno da fixture, o resultado é impresso na própria tabela com cores que indicam o status da execução:

  • Verde: Quando o valor esperado é o mesmo retornado pela Fixture;
  • Vermelho: Quando o valor esperado não é o mesmo retornado pela Fixture;
  • Amarelo: Quando existem erros de formatação na tabela ou os valores da tabela foram informados parcialmente;
  • Cinza: Quando a célula não é executada por algum motivo.

Existem vários tipos de Fixture. Você pode inclusive implementar a sua. No entanto, existem quatro Fixtures básicas, recomendadas principalmente para quem está aprendendo FitNesse. São elas: ColumnFixture, ActionFixture, RowFixture e TableFixture. Veja no gráfico da Figura Q3 como elas se relacionam.

Figura Q3. Fixtures básicas.

Para as tabelas citadas como exemplo na Figura Q2, utilizamos a ColumnFixture, que mapeia as colunas da tabela diretamente para os métodos e atributos da classe Fixture. Esta opção é muito útil quando queremos repetir o mesmo teste com argumentos diferentes.

As demais Fixtures são utilizadas de acordo com a necessidade do usuário. Uma ActionFixture é empregada para testar uma série de ações realizadas pela aplicação e validar o resultado esperado. Ela é ideal quando o comportamento do software que estamos testando é sequencial, isto é, quando uma série de eventos ocorre em uma ordem prescritiva.

A classe TableFixture permite acesso randômico a células da tabela Fit; ideal quando a tabela não é estruturada. Por fim, a classe RowFixture, que é uma especialização da classe ColumnFixture, e foi projetada para validar testes que possuam o resultado originado de pesquisas, como por exemplo, dados retornados por uma query (comando select) ou tabelas que contenham elementos como listas, sequences ou outros conjuntos de dados.

Como era de se esperar, todos os testes falharam, com exceção de um caso em que o retorno esperado era true. Neste momento, vamos iniciar a segunda fase da codificação, agora para os testes unitários, seguindo o conceito do TDD.

É bom deixar claro que os testes de aceitação não devem ser escritos no mesmo nível dos testes unitários, pois isso é considerado uma má prática. Testes unitários são testes específicos de implementação. Nós iniciamos o desenvolvimento pelos testes de aceite, pois eles irão servir apenas como um guia de implementação.

Neste momento, com a ajuda dos testes de aceitação, sabemos o que tem que ser feito, mas não sabemos nada a respeito sobre as classes e métodos da implementação. Como solução para isso, podemos iniciar os testes e deixar que o próprio resultado nos diga como devemos prosseguir.

Como estamos trabalhando em uma estória cuja finalidade é enviar SMS, o ponto mais lógico de se iniciar o desenvolvimento é escrevendo um teste que valide a escrita de uma mensagem SMS vazia. Veja na Listagem 4.

Listagem 4. Primeiro Teste para envio de SMS.
@Test public void mensagemVazia() { Mensagem sms = new Mensagem(); assertNull("Mensagem deveria estar nula!",sms.getMensagem()); }

Primeiro instanciamos uma classe Mensagem, e validamos se o atributo mensagem está nulo. Como era de se esperar, na primeira execução o JUnit nos diz que a classe Mensagem não existe. (Na verdade, antes de executar os testes, a própria IDE já indica que a classe não existe e dá a opção de criá-la para você.) No mesmo momento, criamos a classe mencionada e executamos o teste novamente. Desta vez, ocorre outro erro, agora por conta do método getMensagem() que ainda não existe. De maneira similar, criamos o método requerido, implementando-o da maneira mais simples possível, apenas para fazer com que o teste passe, conforme a Listagem 5.

Listagem 5. Código da classe Mensagem.
public class Mensagem{ public Mensagem(){ } public String getMensagem() { return null; } }

Após fazer o teste passar, na sequência iniciamos um novo método, agora para validar a consistência da Mensagem. Neste ponto, podemos escrever um teste para validar se a mensagem que informamos é válida, de acordo com a Listagem 6.

Listagem 6. Validando a consistência da mensagem.
public void validarMensagem(){ Mensagem sms = new Mensagem("Teste Mensagem"); assertEquals(sms.getMensagem(), "Teste Mensagem"); sms.setMensagem("Outra Mensagem"); assertEquals(sms.getMensagem(), "Outra Mensagem"); }

Ao executarmos o teste recebemos outro erro. Isto porque na classe Mensagem que criamos anteriormente, estamos retornando o valor null no método que deve retornar o valor da mensagem. Para tanto, podemos refatorar a classe para atender a nosso teste, criando um atributo mensagem com seu método setter, e alterar o método getMensagem() para retornar o valor. Dessa forma, em uma nova execução, nossos testes passam com sucesso.

Para evoluir os testes de consistência, neste momento podemos criar um teste para validar se a mensagem é maior que 140 caracteres. Caso seja, devemos receber uma exception, que chamaremos de MensagemInvalidaException. Nosso teste deve estar similar ao código da Listagem 7.

Listagem 7. Evoluindo o teste de consistência da mensagem.
private static final String MENSAGEM_EXCEDIDA = “*********** OLA, ESTA MENSAGEM TEM QUE POSSUIR MAIS DO QUE 140 CARACTERES, POIS ESTE É O MESMO LIMITE DE UMA MENSAGEM NO TWITTER. ***********”; @Test(expected=MensagemInvalidaException.class) public void validarTamanhoExcedido(){ Mensagem sms = new Mensagem(); sms.setMensagem(MENSAGEM_EXCEDIDA); // Mensagem maior que 140 caracteres }

Ao executar o novo teste recebemos outro erro, agora porque a exceção MensagemInvalidaException não existe. Para fazer com que o teste passe, precisamos criar a exceção e refatorar o método setMensagem(String mensagem) da classe Mensagem para que lance esta exceção quando a mensagem for maior do que 140 caracteres. Adicionalmente, podemos validar se a classe permite que informemos uma mensagem nula ou em branco.

Após refatorarmos a classe, recebendo sempre o feedback dos testes, a classe Mensagem deve estar semelhante à Listagem 8.

Listagem 8. Classe Mensagem refatorada.
public class Mensagem{ private String mensagem; public Mensagem(){ } public Mensagem(String mensagem) { this.mensagem = mensagem; } public String getMensagem() { return mensagem; } public void setMensagem(String mensagem) throws MensagemInvalidaException{ if (mensagem == null || mensagem.trim().equals("")){ throw new MensagemInvalidaException("Informe a Mensagem"); }else if (mensagem.length() > 140 ){ throw new MensagemInvalidaException("Mensagem Muito Longa"); } this.mensagem = mensagem; } }

Neste momento já fizemos os testes necessários para validar uma mensagem SMS. O próximo passo é validar o telefone informado, identificando se ele é consistente ou não. Para isso, vamos seguir um dos princípios do XP conhecido como “Baby Steps” ou “Passos de Bebê”, que aplicamos na validação da mensagem, ou seja, para cada pequena mudança em nosso código vamos aplicar um teste e em seguida refatorá-lo.

Primeiro, criaremos um método para testar um número de celular. Para tanto, vamos refatorar nossa aplicação realizando apenas as alterações necessárias para fazer com que os testes passem. Portanto, adicionaremos o atributo celular na classe Mensagem para iniciarmos os testes. Ao final do ciclo, o método de teste deve ser simular a Listagem 9.

Listagem 9. Validando o celular informado.
@Test public void celularValido(){ Mensagem sms = new Mensagem(); assertNull("Celular deveria estar nulo!", sms.getCelular()); sms.setCelular("11 9887-9997"); assertEquals("Celulares deveriam ser iguais!", "11 9887-9997", sms.getCelular()); }

Agora nossa aplicação permite a inclusão de um número de telefone. Porém, ao analisarmos os critérios de aceite da estória (Figura 10), veremos que existe um pequeno problema pois um dos critérios diz que devemos “Validar o número do telefone”. E atualmente nossa aplicação não verifica se o formato do número informado é válido.

Para resolver este problema podemos utilizar expressões regulares (regex), mas como o time conhece pouco sobre regex, antes de iniciar o desenvolvimento podemos fazer uma prova de conceito (Spike). Para aprendermos sobre o correto uso deste artifício vamos criar uma nova classe de teste. Nesta classe, para validar um número de telefone no formato (DDD) 9999-9999, nosso método de teste ficou similar à Listagem 10.

Listagem 10. Teste (Spike) para validar o formato do número de telefone com Regex.
public class AprendizadoRegexTest { @Test public void validaFormato() { Mensagem sms = new Mensagem(); sms.setCelular("(011) 9887-9997"); assertEquals("Celular Inválido!", "(011) 9887-9997", sms.getCelular()); String expression = "^\\(?(\\d)\\)?[- ]?(\\d)[- ]?(\\d)$"; Pattern pattern = Pattern.compile(expression); Matcher matcher = pattern.matcher(sms.getCelular()); assertTrue(matcher.matches()); } }

Na Listagem 10, criamos um objeto do tipo Mensagem, atribuímos um número de celular a ele e realizamos uma validação assertEquals() para nos certificar de que o número informado é consistente; caso não seja, na execução dos testes receberemos a mensagem “Celular Inválido!”. Em seguida, criamos uma expressão regular para validar o celular informado e executamos o teste.

Como nosso spike funcionou, agora precisamos apenas adicionar o código de validação na classe Mensagem. Feito isso, o último teste para fecharmos a estória é o de envio de SMS. Entretanto, ao buscar mais informações sobre como os clientes irão receber o SMS, fomos informados que teremos que utilizar um serviço externo, provido por uma empresa integradora de SMS.

De acordo com a documentação enviada pela empresa, será fornecido um web service para utilizarmos o serviço de envio de SMS. Este serviço possui apenas dois métodos, um para fazermos a autenticação antes de iniciar o seu uso, e outro para envio do SMS.

Integradora de SMS: É uma empresa que disponibiliza o serviço para envio de SMS, onde o cliente não precisa se preocupar com os protocolos e serviços de cada operadora de telefonia.

No livro Growing Object-Oriented Software, escrito por Nat Freeman e Nat Pryce, dois grandes agilistas do Reino Unido e criadores do framework jMock, diz que: “Nunca devemos criar Mocks de tipos que não podemos controlar”.

A razão por trás de tal afirmação é que quando utilizamos bibliotecas de terceiros, como este caso do serviço de SMS, geralmente não temos um entendimento total de como a biblioteca funciona. Mesmo com o código fonte disponível, raramente temos a oportunidade de explorá-lo a ponto de conseguirmos dominar o seu uso. Além disso, o código pode conter bugs ainda não descobertos.

É recomendado também, mesmo com os fontes, não alterar o código da biblioteca externa, pois o custo de aplicar as alterações efetuadas em toda nova versão é muito alto, e isto pode afetar nossos testes, quando houver alterações não previstas. Outro risco evidente é que precisamos ter a certeza de que o comportamento que estamos “imitando” com o mock condiz exatamente com o que a biblioteca externa irá fazer.

Mas se não vamos criar um mock para a API externa, como vamos testar o código que lida com ela? Neste caso, utilizaremos TDD para modelar a interface para os serviços que nossos objetos necessitam. Essa interface será definida de acordo com nossos objetos de domínio, e não com a biblioteca externa. Para alcançarmos este objetivo, podemos criar uma camada Adapter (Padrão de Projeto do GoF) de objetos Mock que utilizam a API de serviço externo para implementar a interface, conforme ilustra a Figura 14.

Figura 14. Adapter de Mocks para acessar serviços de terceiros.

Em Domain Driven Design esta técnica é conhecida como Anti Corruption Layer, que consiste em criarmos uma camada de Adapters e Facades (veja o quadro “Padrão de Projeto: Facade”) para isolarmos o domínio de nossa aplicação de serviços e interfaces externas.

O objetivo desta técnica é efetuar os testes na camada Adapter, focando nos testes de integração, para confirmar o nosso entendimento sobre como a camada de serviço funciona.

Padrão de Projeto: Facade

Facade é um dos padrões de projeto mais populares documentado pelo Gang of Four. Seu objetivo é fornecer uma interface unificada que facilite o acesso a um conjunto de interfaces complexas de um subsistema. Dessa forma, é exposto para o cliente que irá utilizar o Facade apenas uma interface com métodos bem definidos.

Outra vantagem de utilizar o padrão Facade é que ele reduz a dependência entre o cliente (usuário do Facade) e as classes existentes nos subsistemas, aumentando a coesão e reduzindo o acoplamento entre os sistemas envolvidos, conforme ilustra a Figura Q4.

Figura Q4. O padrão Facade em ação.

O padrão Facade pode ser aplicado também como uma camada de conexão entre as camadas lógicas de uma aplicação, servindo como ponto de acesso para um ou mais processos (como apresenta o exemplo da Figura Q5).

Figura Q5. Utilizando Facade para acessar outras camadas da aplicação.

Para iniciar a codificação, vamos utilizar um Adapter para o serviço de SMS. Portanto, criaremos primeiro uma interface para o serviço, para que posteriormente possamos injetá-la em nosso código cliente. Para a interface, definiremos dois métodos, um para autenticação de uso e outro para envio do SMS, como mostra a Listagem 11.

Listagem 11. Interface para o serviço de SMS.
public interface SMSService { public void authenticate(User user) throws AuthenticationException; public boolean send(String phone, String message, String header); }

Para simularmos o serviço de SMS, além do Adapter, criaremos uma classe Factory que possa construir tanto um objeto mock quanto um objeto real, de acordo com a Listagem 12. Nessa classe (SMSServiceMock), implementamos o mock com a mesma interface que usaremos quando formos implementar o serviço da integradora de SMS. Para a versão mock, definiremos apenas o código necessário para simular o envio de SMS.

Listagem 12. Implementação mock para o serviço de SMS.
import com.netfeijao.easypay.domain.User; import com.netfeijao.easypay.exception.AuthenticationException; public class SMSServiceMock implements SMSService{ private User usuario; private boolean authenticated = false; @Override public void authenticate(User user) throws AuthenticationException { if (user == null || (!user.getUser().equals("wagner") || !user.getPassword().equals("t3st3"))){ throw new AuthenticationException("Usuário inválido!"); } this.usuario = user; this.authenticated = true; } @Override public boolean send(String phone, String message, String header) { return true; } @Override public boolean isAuthenticated() { return authenticated; } @Override public User getUser() { return usuario; } } // Factory para construção de objetos SMSService public class SMSServiceFactory { public static SMSService createMockSMSService(){ try{ User usuario = new User("wagner", "t3st3"); SMSService service = new SMSServiceMock(); service.authenticate(usuario); return new SMSServiceMock(); }catch (AuthenticationException ae){ return null; } } }

Agora, para utilizar nosso adapter no módulo de SMS, precisamos alterar a aplicação para aceitar a interface SMSService no construtor da classe Notificador, como ilustra a Listagem 13.

Listagem 13. Alterando o construtor da classe para receber nosso Adapter.
package com.netfeijao.easypay.service; import com.netfeijao.easypay.domain.Mensagem; public class Notificador{ private SMSService service; private static final String HEADER = "EasyPay"; public Notificador(SMSService service){ this.service = service; } public boolean enviar(Mensagem mensagem){ return service.send(mensagem.getCelular(), mensagem.getMensagem(), HEADER); } }

É importante lembrar que durante a criação de todas estas classes que acabamos de explicar, utilizamos o ciclo de testes para validar cada passo que tomamos no desenvolvimento.

Agora, vamos refatorar a classe SMSServiceFit que contém os métodos de execução para o FitNesse – criada na Listagem 3 – para fazer referência à implementação mock do serviço de envio de SMS (SMSServiceMock). O resultado da refatoração deve ficar similar ao código da Listagem 14.

Listagem 14. Classe FIT refatorada.
package com.netfeijao.easypay.fixture; import com.netfeijao.easypay.domain.Mensagem; import com.netfeijao.easypay.exception.MensagemInvalidaException; import com.netfeijao.easypay.service.Notificador; import com.netfeijao.easypay.service.SMSServiceFactory; import fit.ColumnFixture; public class SMSServiceFit extends ColumnFixture { public String celular; public String mensagem; private Notificador notificador = new Notificador(SMSServiceFactory.createMockSMSService()); private Mensagem smsMessage = new Mensagem(); public boolean valido() { smsMessage.setCelular(celular); return smsMessage.validarCelular(); } public String enviar() { try { smsMessage.setMensagem(mensagem); notificador.enviar(smsMessage); return "Mensagem Enviada"; } catch (MensagemInvalidaException ex) { return ex.getLocalizedMessage(); } } // Getters e Setters omitidos. }

Por último, vamos executar os testes de aceitação no FitNesse para validarmos se a aplicação atende aos critérios de nosso usuário. O resultado é apresentado na Figura 15.

Figura 15. Testes de Aceitação após refatoração da classe FIT.

Neste momento a aplicação atende aos critérios impostos pelo usuário, e com o uso do TDD, criamos um código com praticamente 100% de cobertura de testes, garantindo tanto a qualidade interna do código (via TDD) quanto a qualidade externa (via ATDD).

Como alternativa a criação de mocks, poderíamos utilizar frameworks para facilitar os testes em objetos desta natureza. A classe SMSServiceMock, que definimos na Listagem 12, poderia ser criada com auxílio de um destes frameworks. Um framework open source muito conhecido, e que vem atraindo cada vez mais admiradores, é o Mockito.

Para testar a interface SMSService com Mockito, criaríamos um código similar a Listagem 15.

Listagem 15. Utilizando Mockito.
package com.netfeijao.easypay.testes; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.mockito.AdditionalMatchers.*; public class AutenticacaoServicoSMSTest { private SMSService mockService; private User usuario; @Before public void setUp() throws AuthenticationException { mockService = mock(SMSService.class); usuario = new User("wrsantos", "3@sypay"); doThrow(new AuthenticationException()).when(mockService).authenticate(not(eq(usuario))); when(mockService.getUser()).thenReturn(usuario); when(mockService.isAuthenticated()).thenReturn(true); when(mockService.send(anyString(), anyString(), eq("EasyPay"))).thenReturn(true); } @Test(expected=AuthenticationException.class) public void naoDevePassarComSenhaInvalida() throws AuthenticationException{ User outroUsuario = new User("teste", "1234"); mockService.authenticate(outroUsuario); } @Test public void devePassarComSenhaInvalida() throws AuthenticationException{ mockService.authenticate(usuario); assertTrue(mockService.isAuthenticated()); } @Test public void enviandoEmailValido() throws MensagemInvalidaException{ Notificador notificador = new Notificador(mockService); Mensagem msg = new Mensagem("23-8405", "Confirmado Pagamento"); } }

Nessa listagem, fizemos importação estática dos métodos de duas classes: Mockito e AdditionalMatchers. A primeira é a mais importante, pois é a classe que utilizamos para criar o mock e simular o comportamento de uma interface. A classe Mockito estende a classe Matchers, portanto possui diversos métodos para auxiliar na validação dos parâmetros passados.

Matchers: Em bibliotecas mock, Matchers são classes utilizadas para facilitar a validação de parâmetros em métodos invocados. Entre os matchers básicos, podemos citar métodos que avaliam a igualdade e a identidade de um objeto, se um objeto é nulo ou não, se todo argumento é válido, entre muitos outros, sendo possível combinar matchers para chegar ao resultado esperado.

Caso os Matchers básicos não sejam suficientes, é possível utilizar a classe AdditionalMatchers. Na Listagem 15, fizemos uso dos métodos desta classe combinando os métodos not() e eq() para validarmos se o usuário informado é válido.

Conclusões

Na terceira e última parte deste artigo, fechamos o tema de Design e Arquitetura de Software em processos ágeis apresentando a Extreme Programming e cada uma das fases de um projeto. Tratamos de um assunto pouco conhecido por pessoas que iniciam em projetos ágeis, que é entender como é tratado a Arquitetura de Software em um ambiente XP e as práticas utilizadas para apoiar arquitetos de software.

Falamos da importância das primeiras iterações em um projeto ágil para a criação de um ambiente de desenvolvimento, onde contemplamos ferramentas e processos para automação de builds, controle de qualidade, sistemas de controle de versão, bug trackers e gerenciadores de repositórios.

Por fim, simulamos a implementação de uma estória para um sistema fictício, aplicando na prática o Desenvolvimento Dirigido a Testes (TDD), Automação dos Testes de Aceitação utilizando o framework FitNesse, e o uso de Mocks com auxílio do framework Mockito.

Referências

Artigos relacionados