Mostrando postagens com marcador Refactoring. Mostrar todas as postagens
Mostrando postagens com marcador Refactoring. Mostrar todas as postagens

sábado, 11 de abril de 2009

TDD na Prática – Parte V: Testabilidade e a UML

Seguindo nossa pequena série sobre TDD, vamos ver sobre como a testabilidade influencia no design, sobre a diferença de escrevermos os testes antes ou depois e como a UML pode se relacionar com TDD.

(Lembrando que nas próximas partes da série será dado início à implementação de um pequeno e simples Jogo da Velha, nos moldes do TDD)

Como podemos testar um Jogo da Velha ? O que deve ser verificado ?

Esta pergunta pode gerar uma grande lista de coisas que devem ser checadas. Nela, com certeza faltarão coisas simples, óbvias, que serão deixadas de lado pelo fato de geralmente nos atermos às coisas mais importantes, de maior impacto na aplicação. Porém, devemos lembrar que antes de testarmos coisas complexas, temos que ter certeza de que as simples estão funcionando corretamente. Testando-as, teremos segurança para prosseguir para os testes mais complexos, os quais, por exemplo, envolvam a interação de diversos objetos. Neste caso, cada objeto deve ter seu comportamento individual testado, para que o novo teste se concentre na (correta) cooperação entre os mesmos.

QuebraCabeca

Assim, deve-se começar pelo trivial e ir pouco a pouco aumentando a complexidade sobre o que será verificado. Pondere, também, a importância que cada nova verificação tem para a aplicação, do ponto de vista da relação custo-benefício do teste. Veja mais sobre este assunto na Parte IV: Quem, Onde, Quando, O Que e Como.

Complexidade

Fique atento à relação Complexidade do Assunto do Teste versus Complexidade do Código do Teste. Num teste, o o que deve ser verificado pode ser complexo mas não o como. Se seu teste começar a ficar difícil de ser implementado, pare e repense o problema. Pode ser que você precise refatorar seu código de teste. Muitas vezes isto acontece devido ao código criado para o teste estar pouco coeso. Talvez porque ele esteja fazendo mais trabalho do que somente as verificações necessárias, ou talvez porque, na verdade, esteja fazendo mais que somente um teste. Analise com cuidado a responsabilidade atribuída ao método de teste criado e verifique seu é possível refatorá-lo (fazer um Extract Method, por exemplo).

Contudo, se continuar difícil testar uma determinada funcionalidade é porque não deve ser seu código de teste que está ruim, mas, infelizmente, o modelo que você criou/planejou para a(s) classe(s) participante(s) do teste não ficou “testável”.

Testabilidade do Modelo

Este conceito de “testabilidade”, ou seja, do código que você escreveu ser fácil de testar, é um dos principais aspectos do TDD. Isto porque o design de seu projeto muda para se tornar “testável”. Muitas vezes aquele diagrama de classes UML que você criou para ajudar a pensar sobre seu modelo e, sobretudo, o diagrama de seqüência que lhe ajudou a pensar na interação entre os objetos destas classes, irão sofrer alterações significativas.

Esta é a parte em que o TDD começa a guiar o design de seu modelo. Muitas vezes um modelo que parece muito bem feito num diagrama pode ser dificílimo de testar. E por que isto acontece ? Acontece porque os testes forçam seu modelo a expor responsabilidades antes não percebidas, a tornar as dependências entre as classes mais explícitas e também deixam-no mais próximo de como ele será utilizado, o que, no diagrama, pode estar bem longe da realidade (dependendo, claro, da experiência de seu criador).

Daí, os métodos mudam, outros aparecem, classes antes não detectadas surgem, dependências são deixadas visíveis, bem claras e quando você voltar e olhá-lo num diagrama para ver o que se tornou, você terá uma agradável surpresa: seu modelo melhorou !

As verificações forçarão a criação de um modelo mais simples e flexível, mais coeso e bem menos acoplado. Você perceberá quando algo no modelo estiver ruim, pois provavelmente começará a sentir dificuldades para testá-lo. Esta será a hora de adaptá-lo, de torná-lo mais leve, de deixá-lo apto a ser verificado.

Este é o impacto que diferencia criar os testes antes versus criar os testes depois. Você não tem os benefícios da melhoria do modelo se criar os testes depois. E sofrerá um bocado pra fazer alguns testes.

testability

Quando o teste é feito após o código ter sido implementado, você precisará encontrar meios de conseguir retirar a informação que você precisa verificar. Em muitos casos, como a classe não foi pensada para fornecê-la, você precisará alterar seu código ou o código de classes à ela relacionadas. Dependências não poderão ser trocadas por substitutos, pois o modelo não foi planejado para testes. Assim, tudo fica um pouco mais complicado.

Criando os testes antes, você definirá de antemão como obter as informações desejadas e planejará a possibilidade de substituição de dependências, facilitando e acelerando os testes.

UML

Então se os diagramas acabam mudando, qual a necessidade de criá-los ? Creio que a UML é uma excelente ferramenta, sendo válida independente da forma como o projeto seja implementado. Mesmo se o modelo precisar ser adaptado para ficar testável. O exercício de modelagem é importante para se criar uma visão geral do problema, pensar sobre o relacionamento das classes, antever problemas e pré-definir regras de negócio. Um diagrama de classes bem construído, por exemplo, pode servir como um bom ponto de partida para a construção do modelo que será lapidado pelos testes.

Isto pode parecer um pouco contraditório para os puristas de TDD, mas vejo a representação visual como um meio de documentação válido para o projeto. Principalmente diagramas conceituais. É muito mais simples expressar o modelo de um projeto de um forma visual e facilitar a comunicação com os integrantes da equipe. A agilidade da representação visual cai como aquela máxima: “uma imagem vale por mil palavras”.

logo_uml

São vários os benefícios da representação visual e estas justificam o uso de uma ferramenta de sincronização dos diagramas com o código, já que se tornaria inviável a atualização manual dos mesmos. Diagramas de interação, por exemplo, como o de seqüência ou de colaboração, podem sofrer tantas modificações que o custo de mantê-los pode desmotivar sua manutenção.

A contradição entre a UML e o TDD pode ser definida rapidamente assim: Com a UML, você define o modelo através da criação dos diagramas, escreve o código para implementá-los e depois cria os testes. Com TDD, você define o modelo através da criação dos testes e cria o código.

Meu ponto é: crie um rascunho para o modelo através da criação dos diagramas, deixe os testes definirem o modelo final (“testável”), crie o código e sincronize os diagramas. Assim, é tirado proveito dos dois lados.

Documentação Executável

No caso de equipes que não necessitam de diagramas como artefatos para documentação da aplicação, elas ainda podem se beneficiar da “documentação executável” criado pelos testes. Como um manual de utilização do código da aplicação, através do código dos testes é possível aprender a utilizar uma determinada classe e saber exatamente como sua instância se comporta, isoladamente ou em conjunto. Esta documentação estará sempre atualizada, pois os testes são executados freqüentemente, e conterão as regras de negócio e restrições que permeiam o modelo, sendo uma excelente forma (não visual) de conhecê-lo.

Mesmo para as equipes que usam documentação visual, a documentação criada pelos testes deve ser sempre considerada como uma boa fonte de consulta para o conhecimento da aplicação.

sábado, 6 de setembro de 2008

TDD na Prática - Parte III: Abstração, Simplificação e o Ciclo do TDD

Conhecer como verificar as partes do software e estabelecer o nível de abstração para obter os dados a serem verificados é um ponto vital quando se escreve código de teste.

Expor uma interface simples em uma classe e esconder seus detalhes de implementação é imprescindível para alcançar um modelo de fácil reutilização, substituição e que possa ser facilmente testado. Dependências entre classes devem ser tornadas explícitas e bem-definidas, de forma que se possa trocar a classe da qual se depende por outra, sem que haja um esforço adicional.

E, de novo, aplicando os conceitos de acoplamento e coesão e refatorando o código, podemos obter código com a qualidade desejada. A experiência conta muito a favor, é claro, mas tendo em mente a meta de sempre tentar simplificar as coisas ao máximo (KISS- Keep It Small and Simple), é possível chegar a resultados muito satisfatórios.

Só implementar o necessário

O interessante em o design seguir os testes é que você acaba percebendo a quantidade de código que deixa de escrever. Se você se concentra em escrever código que tente resolver um determinado problema da forma mais simples possível, muita coisa que talvez você fosse escrever, pensando que poderia ser útil futuramente, deixa de ser feita - justamente porque você acabou não precisando pra nada (YAGNI - You Ain't Gonna Need It).

Um teste de cada  vez

Às vezes quando estamos criando um teste, pensamos em uma outra coisa que pode ser testada, não exatamente relacionada à parte de código que está sendo testada, e que  podemos adicionar ao teste que está sendo feito agora. Resista a esta tentação. Mantenha cada funcionalidade que precisa ser testada em testes diferentes. Teste uma funcionalidade por vez. Os testes devem permanecer os menores e mais simples possíveis.

Não implemente mais do que o exigido pelo teste

Após escrever o código do teste e executá-lo, ele irá falhar. Afinal, você ainda não escreveu o código que o implementa. Então você começa escrever o código que implementa a funcionalidade e, de repente, bate aquela idéia de escrever um método ou função que você tem certeza absoluta que vai usar no próximo teste. Por que já não deixar pronto o código para o próximo teste, que você tem certeza que vai escrever ? Acontece que você está escrevendo somente o código necessário para passar no teste. Ao escrever um teste de cada vez, você deve implementar uma funcionalidade também a cada vez. Deixe a implementação sempre seguir os testes. Resista a vontade de escrever código sem que algum teste tenha sido escrito para o mesmo antes.

Refatore

Se o código que você irá escrever for muito parecido com algo que você já fez, refatore (DRY - Don't Repeat Yourself). Se você acha que determinado pedaço de código não faz parte da abstração criada (classe, método, função, etc.), refatore. Se um pedaço de código pode ser tornado mais simples e fácil de entender, refatore. Não deixe de aplicar este passo do ciclo. Ele gera um enorme impacto na qualidade e diminui muito o custo da manutenção futura do código.

E outra coisa importante: Refatore os testes também. O código de teste também pode ser simplificado e melhorado, como o código que o implementa.

Em suma, siga o ciclo do TDD

O ciclo do Test-Driven Development se baseia em 3 passos:

  1. Escreva um teste que verifique uma funcionalidade;
  2. Implemente somente o código para passar no teste;
  3. Refatore o código criado;

Respeitando o ciclo, você mantém o código sob controle, não disperdiça tempo criando algo que não vai precisar e mantém o escopo de cada teste sempre pequeno, facilitando seu desenvolvimento e manutenção.

Tente sempre seguir o ciclo. Não sucumba às tentações. ;)

quarta-feira, 13 de agosto de 2008

Glossário Rápido

Eu não espero reinventar a roda e ter que explicar todos os conceitos, metodologias, processos, siglas, etc. que surgirem neste blog. Diversos outros autores já reservaram seu tempo fazendo isso. Meu intuito aqui é prover uma definição suscinta para quem não quer ler todo o conteúdo encontrado no Wikipedia ou no site mais próximo, encontrado talvez numa pesquisa no Google.

Metologias Ágeis

É o nome dado ao conjunto de metologias dissidentes do Manifesto Ágil. Exemplos: Extreme Programming, Scrum, Crystal, Feature-Driven Development, etc.

Manifesto Ágil

Um conjunto de princípios que visa potencializar o relacionamento das pessoas envolvidas no projeto do software (desenvolvedores, gerentes, clientes, etc.) , de forma a trazer benefícios para o mesmo, através de suas relações e práticas.

Práticas Ágeis

Práticas comumente adotadas pelas metodologias ágeis, como Desenvolvimento Incremental, Liberação Frequente, etc.

eXtreme Programming (XP)

Apesar de não parecer, não é nada de novo. Juntou-se práticas antigas de uma forma que pudesse melhorar o desenvolvimento de software, enxugando ao máximo as tarefas burocráticas e que não estavam ligadas diretamente à programação. As práticas foram unidas de forma que uma sustentasse a outra, cobrindo eventuais problemas relacionados à prática isolada das mesmas. O nome "extrema" vem do sentido de que a adoção das práticas é um tanto radical em relação ao desenvolvimento de software tradicional.

Scrum

Outro dissidente do Manifesto Ágil, o Scrum possui uma série de regras, papéis e documentos bem definidos, a serem seguidos pela equipe participante do projeto. Possui terminologia própria (apesar das práticas e papéis não serem novos) que ajudam no entendimento do processo e em sua adoção. Tem foco mais na parte administrativa que na de implementação.

Test-Driven Development (TDD)

Prática em que se escreve código para testar uma funcionalidade no software, antes que o código que implementa esta funcionalidade tenha sido escrito. Envolve diversos outros fatores como design, refactoring, integração, automação de tarefas e diversos tipos de teste, se tornando muito mais abrangente de que a simples definição de sua prática. O TDD é utilizado pela maioria das metodologias ágeis.

Refactoring

Consiste em melhorar o código existente através de sua reorganização, sem que sua funcionalidade seja modificada. A cada tipo de reorganização foi dado um nome (ou frase) para expressar sua intensão e facilitar a comunicação entre os desenvolvedores.

Design Patterns

Os Padrões de Projeto são modelos de organização e interação de classes, percebidos por desenvolvedores de software ao longo do tempo, que foram catalogados e nomeados para facilitar sua indentificação.

Gang of Four (GoF)

Apelido dado ao grupo de autores do livro "Design Patterns - Elements of Reusable Object Oriented Software", composto por Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides. O livro é uma espécie de catálogo de padrões de projeto de software (Design Patterns) , sendo uma ferramenta indispensável aos bons analistas de sistemas e arquitetos de software.

Acoplamento

Nível de inter-dependência entre módulos (ou outras partes) do software. Quanto menor, melhor (mais reutilização).

Coesão

Grau de "singularidade" de um módulo (ou outras partes) do software. Se um módulo desempenha uma única função ou tem um único propósito, ele é altamente coeso.

KISS

Princípio que tem como objeto sempre manter as coisas bem simples. Sua abreviação possui variações, como Keep It Small and Simple ("mantenha as coisas pequenas e simples"), ou - a minha preferida - Keep It Simple, Stupid. ;)

YAGNI

Princípio cujo objetivo é que não se crie coisas das quais você não tem certeza absoluta se vai precisar. Sua abreviação se refere You Ain't Gonna Need It (algo como "você não vai precisar disto").

DRY

Princípio que visa reduzir a repetição, a duplicação, de trabalho. Vem de Don't Repeat Yourself (algo como "não se repita").

Fixture

Conjunto de dados de teste que são compartilhados entre diversos testes.

Outros termos serão incluídos aqui à medida em que forem sendo referenciados pelos artigos do blog.