2. Desenvolvimento Guiado por Testes (TDD)

Uma prévia das modificações que estão sendo feitas em 2. TDD.

2.
Desenvolvimento Guiado por Testes (TDD)

Código limpo que funciona ou “Clean code that Works” [BECK, 2002], foi para alcançar este objetivo que o TDD surgiu. Um código será tanto mais limpo quanto melhor for sua arquitetura e menos erros ele tiver.

Uma forma de sempre ter código limpo que funciona, é utilizar o desenvolvimento fortemente guiado por testes automatizados que são escritos antes do código das funcionalidades, cujo objetivo principal é a melhoria contínua da arquitetura e diminuição dos erros. Este estilo de desenvolvimento foi chamado de Desenvolvimento Guiado por Testes ou Test-Driven Development (TDD), uma técnica para desenvolvedores e não para testadores.

TDD consiste de duas regras principais: Código novo só é escrito se um teste automatizado falhar; Refatore somente o código que possui testes automatizados e sem falhas.

São duas regras simples, mas que tem conseqüências complexas no comportamento individual e coletivo dos desenvolvedores. Elas impõem um ritmo constante e incremental baseado em passos pequenos (“baby steps”). O passo inicial é pensar um pouco sobre o que será desenvolvido e elaborar uma pequena lista dos testes que serão implementados. Depois disso, cada teste deve ser implementado, um por vez, de acordo com os seguintes passos principais, representados por três cores:

  • Vermelho – Escreva um pequeno teste, faça-o compilar e veja-o falhar. O teste deve necessariamente falhar, caso contrário estar-se-á implementando algo irrelevante;
  • Verde – Escreva o código mais simples possível que seja suficiente para fazer o teste passar. É permitido cometer alguns pecados, como os listados em livros sobre refatoração como [FOWLER, 2004]. Ou seja, deve-se preocupar somente em fazer o teste passar, nada mais;
  • Refatore – Torne o código mais fácil de entender e modificar. Deve-se começar eliminando todas as duplicações. O objetivo é evoluir em direção a um design simples, reduzindo o acoplamento e aumentando a coesão.

Vermelho / verde / refatore — O mantra do TDD”. [BECK, 2002]

Passos do TDD

 

2.1. Design

O objetivo mais importante do Desenvolvimento Guiado por Testes é a melhoria contínua da arquitetura. Com TDD a arquitetura evolui junto com os testes, ou seja, em passos pequenos e de forma a atender apenas aos requisitos das funcionalidades que estão sendo implementadas.

Cada teste representa de forma extremamente pragmática, o reflexo de algo que necessariamente deve ser feito para atingir um objetivo bem definido, e só deve passar (ficar verde) se este objetivo for alcançado. Em função disso, o desenvolvedor é induzido a primeiro focar-se nos objetivos e refleti-los nos testes, e só depois pensar no código que alcançará estes objetivos. Isto traz grandes benefícios para o design do sistema e é bastante eficiente, pois reduz a possibilidade de se escrever código desnecessário: se passaram (ficaram verde) todos os testes possíveis para um objetivo, é porque este foi totalmente implementado.

“Escrevendo os testes primeiro, você se coloca no papel do usuário do seu código, ao invés de desenvolvedor. A partir desta perspectiva, você normalmente consegue obter um sentimento muito melhor sobre como uma interface será realmente utilizada e pode ver oportunidades para melhorar seu design (HUNT & THOMAS, 2003, p.115, tradução nossa)” [TELES, 2005].

Quando está difícil de escrever um teste automatizado é sinal que existe um problema no design e não nos testes, isto acontece porque quanto pior o design, mais difícil será escrever os testes. Em contrapartida, quanto menor o acoplamento e maior a coesão do código, mais fácil será testá-lo, ou seja, mais fácil será o trabalho do desenvolvedor. Por isso, o simples compromisso em criar testes automatizados induz o desenvolvedor a preocupar-se continuamente em criar código simples e com melhor design possível.

[BECK, 2005] [SATO] [TELES, 2005]

2.2. Documentação

Os testes automatizados implementados antes das funcionalidades ajudam a equipe de desenvolvimento a documentar o código, pois são excelentes para expressar intenções de forma clara, além de como os elementos devem se comunicar e acoplar.

Ao usar TDD o desenvolvedor é estimulado a pensar no código (classes, métodos, etc.) necessário para as funcionalidades, como se ele já estivesse implementado. Desta forma, o único desafio passa a ser documentar através das classes de testes, o que o código hipotético deve realmente implementar.

Os testes representam exemplos de utilizações do código e, portanto, podem ser bastante úteis para ajudar a compreendê-lo. Além disso, “ao contrário da documentação escrita, o teste não perde o sincronismo com o código (a não ser, naturalmente, que você pare de executá-lo) (HUNT & THOMAS, 2003, p.6-7, tradução nossa).” [TELES, 2005].

“Não chega a ser surpresa que seja difícil, se não impossível, manter a documentação precisa sobre como o software foi construído. (…) Entretanto, se um sistema possui uma base de testes abrangente que contenha tanto testes dos desenvolvedores, quanto testes dos clientes, estes testes serão, de fato, um reflexo preciso de como o sistema foi construído. Se os testes forem claros e bem organizados, eles serão um recurso valioso para compreender como o sistema funciona a partir do ponto de vista do desenvolvedor e do cliente (POPPENDIECK & POPPENDIECK, 2003, p.148-149, tradução nossa).” [TELES, 2005].

Pelos motivos explicados, os testes substituem de forma muito mais eficiente as representações, sejam em forma de diagramas ou em forma de textos, das classes e das iterações entre os objetos destas. A única exceção é para os esboços “UML” [FOWLER UML, 2004], que podem ajudar bastante no planejamento e na organização das idéias iniciais, mas que depois de refletidos no código em forma de testes, perdem a utilidade e normalmente são descartados.

 

2.3. Verificação e Prevenção

A existência de erros e suas reincidências constituem historicamente, um dos problemas mais tradicionais do desenvolvimento de um software. Além disso, desenvolver software de forma iterativa e incremental, como pregam as metodologias ágeis, gera o risco adicional de se introduzir falhas em algo que vinha funcionando corretamente. A adoção do desenvolvimento orientado por testes minimiza a insegurança em relação à existência ou não de erros no software. “O desenvolvimento orientado a testes é uma forma de lidar com o medo durante a programação (BECK, 2003, p.x, tradução nossa).” [TELES, 2005].

Os testes automatizados procuram comprovar que as solicitações dos usuários estão sendo atendidas de forma correta. Além disso, os testes verificam se o código está fazendo o que deveria ao longo do tempo, pois “(…) além de assegurar que o código faça o que você quer, você precisa assegurar que o código faça o que você quer o tempo todo (HUNT & THOMAS, 2003, p.5, tradução nossa).” [TELES,2005].

TDD leva os desenvolvedores a criar uma base de testes automatizados que devem ser executados toda vez que um novo fragmento de código é adicionado ao sistema. Embora isso não impeça a ocorrência de erros, representa um instrumento útil para detectá-los rapidamente, o que agiliza a correção e evita que eventuais erros se acumulem ao longo do tempo [TELES, 2005].

2.4. Custos

As fases de desenvolvimento e depuração ocupam a maior parte do tempo de um projeto, sendo que a depuração geralmente só é feita quando o software apresenta um ou mais erros (Bugs). O que torna a depuração muito custosa é o tempo entre a inserção do erro e o momento em que este é detectado. Quanto maior o tempo, maior o custo de depuração, porque para corrigir um problema, o desenvolvedor precisa recuperar o contexto em que este foi inserido, entender uma série de coisas relacionadas, além de descobrir o que pode estar gerando o erro. Ou seja, é preciso re-aprender sobre a funcionalidade para que se possa corrigi-la.

O desenvolvimento Guiado por testes segue o caminho da prevenção. Para isso é preciso incorporar hábitos que resultem numa menor probabilidade de ocorrência de erros. Mesmo assim, é possível que erros ocorram. Neste caso, os testes fazem com que a correção do erro seja mais barata por dois motivos: 1 – O teste expõe erros assim que eles entram no sistema, o que evita muita perda de tempo com depurações demoradas; 2 – Caso um erro seja introduzido em uma parte do sistema diferente daquela que se está trabalhando no momento, os testes expõem o local exato do erro, permitindo que este seja encontrado e corrigido muito mais rapidamente.

 

2.5. Aprendizado

Quanto maior for o tempo entre a inserção de um erro no código e sua descoberta pelo desenvolvedor, maior será a probabilidade deste erro repetir-se em outros locais do sistema e menor a possibilidade de aprendizado. Por outro lado, se o erro é descoberto alguns segundos após ser introduzido, são maiores as chances do desenvolvedor corrigi-lo rapidamente, aprender com isto e passar a codificar melhor. Conseqüentemente reduz-se muito as chances de problemas semelhantes repetirem-se no futuro.

Para um melhor aprendizado e redução da probabilidade de propagação de erros, é fundamental que o ambiente de desenvolvimento forneça respostas rápidas para pequenas mudanças. TDD expõe o erro assim que ele é inserido, garantindo uma forma de retorno rápido sobre as mudanças efetuadas.

 

2.6. Produtividade

Devido a grande complexidade envolvida no processo de desenvolvimento de software é comum ocorrerem erros dos mais variados tipos. É impossível evitar que estes erros ocorram ao longo de um projeto, entretanto é possível fazer alguma coisa em relação à quando estes defeitos são detectados e qual o impacto que causarão no cronograma do projeto. Só isto, causa um grande impacto na velocidade de desenvolvimento e na qualidade do software.

Observando-se o trabalho de um desenvolvedor, nota-se que boa parte do seu tempo é dedicada à depuração. Trata-se de uma atividade freqüentemente demorada que se repete inúmeras vezes à medida que o desenvolvedor produz mais código para o projeto.

Sempre que um teste detecta uma falha rapidamente, evitam-se longas sessões de depuração que costumam tomar boa parte do tempo dos desenvolvedores. Com mais tempo disponível e melhores oportunidades de aprendizado, os desenvolvedores codificam mais rapidamente e com maior qualidade, ou seja, aumenta-se a produtividade e reduz-se a incidência de defeitos. “Descobri que escrever bons testes acelera enormemente a minha programação(…). Isso foi uma surpresa para mim e é contra-intuitivo para a maioria dos programadores (FOWLER, 2000, p.89, tradução nossa).”[TELES, 2005].

Por tudo isto, escrever os testes antes das funcionalidades traz um aumento na produtividade, pois evita desperdícios futuros com depuração e torna o código mais claro e bem documentado.

2.7. Testes de Unidade

Um teste de unidade é um trecho de código com a característica especial de ser escrito para executar outro trecho de código, e determinar se este outro trecho de código está se comportando como esperado ou não.

Para conferir se o código está se comportando como esperado, geralmente usa-se uma afirmação, ou seja, uma chamada de método simples que verifica se algo é verdadeiro ou não. Por exemplo, o método IsTrue verifica se uma determinada condição booleana é verdadeira.

Como já foi explicado, quanto mais tempo se passar entre o momento em que o erro é introduzido e o momento em que é identificado, maior tende a ser o tempo de depuração. É o retorno (feedback) imediato gerado pelos testes de unidade que ajuda a reduzir este tempo e, portanto, a acelerar a depuração. Mas, isso só é possível se os testes forem automatizados.

Um teste unitário não é bom quando: Comunica-se com o Banco de Dados; Faz algum tipo de comunicação na rede; Comunica-se com o sistema de arquivos; Não pode ser executado ao mesmo tempo em que outros testes (dependência entre testes); É preciso fazer alterações especiais no ambiente para poder executá-lo. [FEATHERS, 2004]

Um teste unitário deve preocupar-se unicamente com o trecho, um método geralmente, de código que está sendo testado. Não deve depender, se preocupar, com outras partes do sistema. Um teste unitário verifica a funcionalidade, não a implementação.

2.8. Testes de Aceitação

Os testes de aceitação servem de documentação para os requisitos e promovem o desenvolvimento guiado pelos objetivos a serem alcançados, melhorando o design do sistema. Além disso, verificam o funcionamento integrado das classes do sistema e indicam, do ponto de vista do cliente, quando as funcionalidades estão prontas. Também servem como uma ferramenta que alerta sempre que algum requisito deixar de ser atendido.

Devem ser escritos pelos clientes ou refletir o que os clientes pensam, pois são eles que conhecem o negócio e, portanto, os objetivos que devem ser alcançados. Os testes de aceitação falam numa linguagem que o cliente entende.

Os testes de aceitação “são escritos para assegurar que o sistema como um todo funciona. (…) Eles tipicamente tratam o sistema inteiro como uma caixa preta tanto quanto possível (FOWLER, 2000, p.97, tradução nossa).” [TELES, 2005].

2.9. xUnits

Existem diversos frameworks para testes unitários que derivam de uma arquitetura para testes conhecida como xUnit. Os principais xUnits da atualidade são: NUnit (www.nunit.org) para .NET e JUnit (www.junit.org) para JAVA.

É possível escrever testes sem a ajuda de tais frameworks, mas não é possível desenvolver usando TDD sem eles, principalmente devido a perda de agilidade que isso representa.

Entre as principais funcionalidades oferecidas pelos NUnit e JUnit estão: Um conjunto amplo de asserções para testar resultados esperados; Interface gráfica e textual para execução de testes; Integração com os principais ambientes de desenvolvimento (IDEs); Grande comunidade de usuários.

 

2.10. Mocks

São objetos falsos usados para testar unidades individuais de código sem testar suas dependências. O termo “Mock Object’ se tornou popular popular como objetos que simulam objetos do mundo real para testes.

 

\o ‘s,
Vinicius AC.

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s