msdn09_capa.JPG

Clique aqui para ler todos os artigos desta edição

 

TEST-DRIVEN C#

Melhore o design e a flexibilidade de seu projeto com técnicas de Extreme Programming
por Will Stott e James Newkirk

 

Você não adoraria criar um código que se tornasse cada vez mais fácil à medida que o projeto amadurecesse, em vez de o contrário? Parece que, independentemente de quanto cuidado você tome, mais cedo ou mais tarde seu código se tornará um tanto confuso. Quanto maior o projeto, pior se torna o problema. Quantas vezes você iniciou um projeto com um design quase perfeito apenas para ver ele se desfazer após o início da codificação?

  O TDD (Test-driven development) altera o processo de escrita de código de modo que a alteração do projeto não somente seja possível, como desejável. O desenvolvimento gira em torno de três atividades básicas: escrever um teste, escrever o código a ser passado ao teste e reescrever o código para banir duplicações e torná-lo mais simples, flexível e fácil de ser compreendido.

  Este ciclo é repetido várias vezes e, a cada vez, ele executa todos os testes para assegurar que o produto seja mantido em um estado funcional. Acabaram-se os longos intervalos entre as fases de design, codificação e teste, o que possibilita um ambiente de desenvolvimento muito melhor. Desse modo, seu design (e código) na verdade melhoram cada vez mais à medida que o projeto amadurece, e não o contrário.

  O que torna o TDD tão eficaz é a automação de seus testes (unit testes) de programação, e a melhor notícia é que as únicas ferramentas de que você precisa estão disponíveis gratuitamente na Internet. Elas não são versões de um produto comercial com funcionalidade reduzida, mas sim softwares de alta qualidade disponibilizados por colegas desenvolvedores. Este artigo explica como você pode obter e usar o NUnit para praticar o TDD durante o desenvolvimento com C# (ou de qualquer linguagem baseada no Microsoft® .NET Framework). Observe que existem ferramentas similares disponíveis para desenvolvedores em C++ e Java, bem como para muitas outras linguagens e sistemas operacionais. A pronta disponibilidade dessas ferramentas fornece ao TDD um atrativo universal, o qual, combinado com sua íntima associação com o extreme programming, contribuiu muito para encorajar seu uso.

 

Por que os designs degradam

A maioria dos processos de software parte do princípio de que você pode conseguir todo o design desde o início e apenas conduzi-lo pela máquina de desenvolvimento para gerar um produto perfeito. Essa é uma mentalidade de linha de produção, que valoriza a uniformidade e minimiza a variação. Tais processos não valorizam a comunicação e o feedback da mesma maneira que o TDD faz, e por isso são menos eficientes tanto para gerar informações (testes de falha) como para permitir que as pessoas aprendam com elas (consertando o design).

Por que você não consegue obter o design correto desde o início? Porque no início do projeto você ainda não tem um conhecimento perfeito a respeito do software que está desenvolvendo. O desenvolvimento interativo reconhece esse fato e o ajuda a identificar problemas significativos no início do projeto, em vez de deixar que sejam descobertos no final. No entanto, a interação não será cancelada para permitir que você retorne à fase do design e resolva um problema simples como uma classe pública nomeada de forma incorreta. Ninguém quer escutar essas preocupações menores e, infelizmente, o processo é geralmente projetado para suprimi-las, devido ao alto custo de movimentação entre as fases.

O acúmulo desses pequenos problemas é responsável pelas dificuldades reais em muitos processos de desenvolvimento tradicionais. Você sempre terá prioridades mais importantes do que fazer alterações que não ofereçam nenhuma vantagem funcional. No entanto, quanto mais tempo a classe nomeada incorretamente permanecer na base do código, mais predominante se tornará seu uso e mais trabalhosa será a tarefa de alterá-la. Depois de um tempo, a equipe começará a corrigir esses tipos de problemas extra-oficialmente durante a codificação, o que significa que logo você estará planejando uma versão completa com poucas diferenças em relação à original além de tentar fazer com que o código corresponda à documentação do design, ou vice -versa. Nesse estágio, seu design feito anteriormente será inútil — é o código que manda.

O TDD permite que você adie as decisões até que compreenda melhor o problema. Você não precisa produzir um design de arquitetura perfeito desde o início, quando tem pouco conhecimento sobre como o produto será desenvolvido. Isso desafia a maioria das idéias estabelecidas sobre desenvolvimento de software e é até certo ponto antiintuitivo, por isso sugerimos que você tente o TDD com uma mente aberta e descubra seu poder por conta própria.

Os benefícios do TDD também são muito mais fáceis de compreender. Ele estimula a comunicação ao tornar seus programas autodocumentáveis — o conjunto de testes que você desenvolve mostra o funcionamento do código de uma maneira impossível de ser mostrada por um manual. Ele encoraja o feedback ao solicitar que você visualize seu código pela perspectiva de alguém que esteja escrevendo um teste para ele, o que ajudará você a criar objetos e componentes que sejam mais pouco acopladas (loosely coupled) e coesos. Ele incentiva a simplicidade ao permitir que você adie grandes decisões para se concentrar primeiro nas pequenas decisões. Por fim, ele lhe dá coragem para ultrapassar os limites de seu código, fornecendo-lhe um conjunto de testes que informam imediatamente quando alguma coisa falha.

 

Uma introdução simples

A única maneira de realmente compreender o TDD é fazendo, por isso vamos começar com um exemplo bastante simples, que não requer nenhuma ferramenta especial. Vamos escrever um pequeno programa que nos ajudará a planejar um terreno retangular para uma casa, mas antes de iniciarmos, existem duas coisas que queremos testar: calcular a área como 6 unidades, onde o comprimento é 3 unidades e a largura é de 2 unidades, e calcular também o perímetro como 10 unidades, para um comprimento de 3 unidades e largura de 2 unidades.

Anotar essas etapas ajudará a concentrar sua atenção no que é importante em nosso programa. Neste caso, parece razoável desenvolver um objeto que ajudará a dar forma ao domínio do problema, então criaremos uma classe chamada Quad, nomeada para o terreno quadrangular que estamos tentando construir, e a instanciaremos como um objeto em uma simples aplicação de Console. Veja aqui as etapas a seguir:

1.                Inicie o Visual Studio®, selecione o menu File | New | Project e, em seguida, escolha "Console Application" na lista de tipos de projeto em C#. Digite "QuadApp" na caixa Project Name e pressione OK.

2.                Na função Main, digite algumas linhas de código para criar uma instância de Quad e, em seguida, declare que quando o método Area do objeto receber um comprimento 3 e uma largura 2, ele retorne o valor 6:

 

{}  static void Main(string[] args)

 {

     Quad q = new Quad();

     System.Diagnostics.Debug.Assert(q.Area(3,2) == 6);

 }

 

3.                Selecione Project | Add Class, digite o nome "Quad.cs" na caixa de diálogo e pressione Open para criar a classe.

4.                Digite o código necessário para dar a Quad uma função Area conforme solicitada pelo seu uso em Main:

 

public class Quad

 {

     public int Area(int length, int width)

     {

         return 0;

     }

 }

 

5.                Selecione o menu Build | Build QuadApp e, desta vez, o processo de criação deverá ser bem-sucedido. No entanto, quando você executar o programa (Debug | Start), ele irá se expressar porque Area não retorna 6.

6.                Codifique um valor de retorno 6 para a função Area, faça o rebuild do programa e torne a executá-lo. Desta vez, o programa será executado sem reclamações.

7.                Melhore a implementação e o design de Area retornando o produto de seus parâmetros de entrada em vez de codificar um valor de retorno. Verifique se você não danificou nada, recriando o programa (rebuild) e executando novamente o teste.

 

O segundo teste (calcular o perímetro como 10 unidades) funciona de maneira similar. Comece por escrever o teste usando a declaração (assert) em Main e, em seguida, siga as Etapas 4 a 7, mas desta vez para Perimeter, em vez de Area. Ao tentar melhorar o design (Etapa 7), você provavelmente chegará à conclusão de que talvez devesse estar passando o comprimento e a largura para o construtor e armazenando-os como propriedades do objeto, em vez de passá-las como parâmetros para as funções do membro. Altere Quad de modo que ele armazene o comprimento e a largura como propriedades (mostradas em Listagem 1), recrie o programa (rebuild) e, em seguida, execute-o novamente para assegura-se de que não tenha danificado nada.

 

Listagem 1 Quad reescrito

public class Quad

{

    private int m_length;

    private int m_width;

 

    public Quad( int length, int width)

    {

        m_length = length;

        m_width = width;

    }

    public int Area()

    {

        return m_length * m_width;

    }

}

 

Completamos um exercício que demonstra o TDD em sua forma mais simples. Veja um resumo de cada etapa envolvida:

Escrever um teste que falhe - Selecionamos o teste que parecia mais fácil de implementar (embora neste caso eles fossem todos simples) e o escrevemos. Em seguida, escrevemos a implementação mais simples do Quad para que o programa pudesse compilá-la. No entanto, quando executamos o programa, ele reclamou porque Area não retornou o valor 6.

Fixar o código para que você passasse no teste - Para fixar o código, executamos a tarefa mais simples e definimos um valor de retorno 6. Como o objetivo era apenas colocar o programa em funcionamento, ele passou no teste. No entanto, o próximo teste pode usar diferentes valores de parâmetro, o que nos obrigará a implementar um algoritmo apropriado para que possamos passar por ambos os testes, mesmo que não os reescrevamos conforme descrito a seguir.

Reescrever o código - Como obtivemos o comportamento correto em nosso programa, tentamos facilitar ao máximo a manutenção do código, removendo duplicações e tornando mais simples, flexível e fácil de compreender. Para confirmamos que essa alteração não havia alterado o comportamento desejado do programa, executamos novamente o teste.

A idéia de retrabalhar o seu código para melhorar a capacidade de mantê-lo sem alterar o comportamento observado no programa não é nova. No entanto, o reescrever envolve mais do que apenas organizar o código de tempos em tempos. Deve se tornar uma parte-chave da atividade de desenvolvimento de software. Ele deve ser feito de maneira metódica, com as ferramentas apropriadas, de modo a melhorar de forma progressiva a qualidade do código. O objetivo é obter um software que possa ser perfeitamente mantido, executando-se várias pequenas correções do código durante a vida útil do produto.

Reescrever consiste em uma etapa essencial no TDD e lhe fornece o feedback necessário para melhorar seu design (e código) à medida que desenvolve o produto. Você pode, no entanto, refazer sem executar o TDD e ainda assim banir as duplicações e tornar o código mais simples, flexível e fácil de compreender. Você só precisa fazer da reescrita uma atividade regular em qualquer processo de desenvolvimento que utilize e certificar-se de que suas alterações sejam validadas por meio dos testes de regressão apropriados.

O poder de reescrever reside na sua capacidade de reduzir o perigo inerente durante as alterações no código de trabalho. Boas ferramentas podem ajudar a contornar alguns dos riscos potenciais, mas igualmente importante é a execução de uma série de etapas pequenas e utilizar uma abordagem disciplinada e estruturada. O livro Refactoring: Improving the Design of Existing Code, de Martin Fowler (Addison-Wesley, 1999), oferece uma boa introdução a essas questões, fornecendo um catálogo útil de padrões de reescrita e guiando o leitor através do processo de reescrita por meio de vários exemplos relevantes.

Até o presente momento, não existe muito suporte a reescrita no Visual Studio .NET, além do uso do Find e Replace. No entanto, existem ferramentas mais úteis a caminho, que permitem que você faça pequenas coisas como renomear simbolicamente e alterar o nome de uma classe no nível da árvore de análise gerada por compilador, em vez de apenas executar uma substituição textual em seus arquivos de origem. Você pode esperar o dia em que poderá selecionar um fragmento de código, aplicar um modelo de reescrita a partir de um menu fornecido pelo Visual Studio e, em seguida, prosseguir com seu trabalho — confiante de que criou um programa mais fácil de manter sem que precisasse introduzir quaisquer bugs ou alterar seu comportamento original.

 

Lições aprendidas

Até aqui, a lição mais importante que você aprendeu é que o TDD é simples. Na verdade, o TDD é ótimo para fornecer uma melhor compreensão a respeito de qualquer nova linguagem, tecnologia ou componente que surja em seu caminho. Ele permite que você decida o tamanho das etapas que precisa seguir. Um expert poderia ter escolhido etapas maiores e, por meio da experiência, evitado algumas das etapas intermediárias que nós utilizamos. Entretanto, se utilizar etapas menores, quando as coisas começarem a dar errado você sempre terá a opção de retornar e refazê-las. Veja a seguir algumas outras características que você deve ter notado no TDD:

Ø      Os testes documentam o código. Podemos ver exatamente o que Quad faz observando os testes.

Ø      Podemos medir o progresso pelos testes passados. Cada parte da funcionalidade é constatada por meio de um teste, desse modo qualquer pessoa pode executar os testes para ver que as ações de progressos estão bem fundamentadas.

Ø      Os testes deixam os usuários seguros em relação à alteração do código. Mesmo que você seja um programador em C# novato, e esteja trabalhando em uma classe escrita por um expert, executar todos os testes o deixará seguro de que não quebrou nenhum código.

Ø      Erros como passar o comprimento e a largura como parâmetros para Area e Perimeter (em vez de para o construtor) são corrigidos reescrevendo.

Ø      Quando você aplica o TDD no mundo real, deve se preparar para gerar muitos testes, por isso precisará organizá-los com uma ferramenta como a NUnit.

 

Automatizando seu teste com NUnit

  Um projeto TDD pode gerar milhares ou até mesmo centenas de milhares de testes. Partindo do princípio que qualquer pessoa da equipe estará "escrevendo um teste, fixando o código e reescrevendo o ciclo freqüentemente, é essencial que você possa tanto escrever os testes como executá-los de forma eficaz. Um framework de teste como o NUnit é projetado para ajudá-lo nessa tarefa. Ele permite que você reorganize seus test cases em projetos individuais, que possam ser carregados no NUnit da mesma maneira que você cria projetos no Visual Studio. Você também pode exibir todos os test cases do projeto em uma hierarquia, executar testes individualmente ou em conjunto (suite) e ver o resultado da execução na forma de um “pass” (verde) ou “fail” (vermelho), com informações detalhadas sobre cada falha (veja a Figura 1).

 

image001.gif

Figura 1 Status de teste no NUnit

 

Além disso, o NUnit fornece funções especiais que você pode executar no início de um conjunto de testes e após o seu término para inicializar e limpar "dispositivos de teste" ("test fixtures"), tais como arquivo de log, conexão de banco de dados, link comm, compartilhados por diferentes test cases. Você também pode especificar uma função Setup que seja executada antes da execução do test case e uma função TearDown que seja executada após o término do test case. Isso o ajudará a isolar os test cases, redefinindo o sistema entre a execução de cada um deles.

O NUnit lhe permite escrever um test case usando a mesma linguagem e ambiente utilizados em seu aplicativo. Como não há nenhuma linguagem de teste especial a ser aprendida, o desenvolvimento de testes é rápido e objetivo. Existe também uma classe Assert com amplos recursos que fornecerá informações sobre o motivo pelo qual um determinado teste falhou.

Por fim, NUnit pode ser executado como um aplicativo de console, que simplesmente escreve seus resultados no prompt de comandos. Isso permite que você automatize um processo de construção formal de modo que possa, por exemplo, recriar o aplicativo durante a noite e executar o conjunto completo de testes. O aplicativo de console NUnit também pode produzir um log XML dos resultados do teste.

Os usuários com experiência anterior em testes de software apreciarão o valor desses recursos, mas mesmo que você não tenha esse nível de experiência, deve saber que o NUnit tem muito a lhe oferecer como desenvolvedor ( veja a terminologia de teste na Tabela 1). Apesar disso, seja você um expert ou um novato, a melhor maneira de descobrir como o NUnit poderá ajudá-lo a organizar seus testes é experimentando o programa. Você pode fazer o download do NUnit em http://www.nunit.org. O arquivo de licença do produto fornece detalhes precisos sobre o que é permitido fazer com o programa.

 

Tabela 1. Terminologia de Teste

Test case

Uma unidade de teste, da mesma maneira que uma função é uma unidade de código. Um test case geralmente contém o objeto de teste, os vários dispositivos (fixtures) de que ele necessita e as instruções e as declarações que compõem o teste.

Ambiente de teste

O estado (conhecido) do sistema no qual você está executando os testes. Geralmente, é uma versão mais simples e controlável do ambiente de produção no qual o produto irá efetivamente operar.

Test fixture

Um objeto compartilhado por um ou mais test cases interessados em inicializar o objeto de teste ou em fornecer a ele algum recurso.

Test framework

Uma biblioteca que facilita a escrita e execução de test cases, tais como as diversas classes fornecidas pela nunit.framework.dll.

Test object

O objeto que está sendo testado, geralmente uma instância da classe que você está desenvolvendo.

Script de teste

(Test script)

Arquivo contendo um ou mais test cases e qualquer outra informação que possa ser solicitada para a criação de um programa de teste, tais como os arquivos de código-fonte do programa de teste.

Suíte de testes (Test suite)

Um conjunto de test cases (ou suítes) que são associados a um componente específico, assembly ou área do produto. Quando você executa um conjunto de testes completo, está na verdade executando todas as suítes de teste do aplicativo.

Ferramenta de teste

(Test tool)

Um aplicativo voltado à execução de test cases e suítes, por exemplo, o aplicativo NUnit.

 

Neste artigo, usei a versão beta do NUnit 2.1, mas você pode fazer o download do arquivo MSI da última versão em produção (aproximadamente 1,5 MB). Depois de descarregar esse arquivo em seu PC, você só precisará clicar duas vezes sobre ele para iniciar o Windows® Installer. Ele funcionará tanto com a versão 1.0 como 1.1 do Microsoft .NET Framework, embora você deva consultar a documentação do NUnit para obter mais informações sobre os requisitos de sistema.

Após instalar o NUnit, selecione o submenu Test para executar os testes que confirmam que o produto está funcionando apropriadamente. Apenas clique na opção do menu, aguarde até que o NUnit seja aberto na área de trabalho e, em seguida, pressione o botão Run.

Em menos de um minuto, todos os nós na treeview deverão estar verdes, o que indica que os testes foram executados e foram bem-sucedidos. O número de testes executados é mostrado na barra de status, juntamente com o tempo levado para executar os testes e o número de falhas. No caso (improvável) de obter alguns nós vermelhos (que indicam falhas no teste), é possível que você precise reinstalar o produto ou procurar auxílio no site NUnit.org.

 

Levando o NUnit para uma rodada de testes

O NUnit oferece vários exemplos para você começar, e vamos acompanhar um caso de desenvolvimento usando um deles, mais precisamente o projeto Money.

Inicie o Visual Studio em sua área de trabalho, selecione File | Open | Project e, em seguida, selecione o arquivo file Money.csproj, que está no diretório de instalação do NUnit (Program Files\NUnit) no subdiretório src\samples\money. Pressione Open e aguarde enquanto o projeto é carregado no Visual Studio. Na janela Solution Explorer, olhe a pasta References e procure por nunit.framework. Para criar o projeto Money, escolha Build | Build Money. O processo de criação deverá ser bem-sucedido, sem erros ou advertências.

Inicie o NUnit na sua área de trabalho. A treeview deverá estar vazia — caso contrário, feche todos os projeto existentes, selecionando File | Close. Abra o arquivo Money.csproj, escolhendo File | Open (de maneira semelhante a utilizada para abri-lo no Visual Studio). A treeview deverá agora conter um conjunto de test cases.

Selecione o nó-raiz na treeview (o arquivo Money.csproj) e clique no botão Run no painel direito de NUnit. O conjunto completo de test cases será executado e você deverá ter apenas um teste com falha —MoneyBag Equals.

Selecione o test case MoneyBag Equals (o nó de folha vermelho) e localize a linha problemática em cada arquivo. Essas informações são fornecidas na parte inferior do painel direito do NUnit, mostrado na Figura 2.

 

image002.gif

Figura 2 Rastreamento de erros no NUnit

 

Alterne para o Visual Studio e abra o arquivo problemático na linha apropriada. Comente a linha (insira // no início da linha) e crie novamente o programa (Build | Rebuild Money). O processo de criação deverá ser bem-sucedido, sem erros ou advertências. Agora, alterne para NUnit e repare que os nós da treeview estão todos cinzas, o que indica que ainda não foram executados test cases para o programa que você acabou de atualizar. Em seguida, selecione o nó-raiz na treeview e clique novamente no botão Run. Você deve observar que todos os testes estão sendo executados sem falha. E isso é tudo o que é preciso saber para trabalhar com o NUnit. Você está agora pronto para começar a escrever test cases para seus próprios projetos.

 

Test Cases

O test case aplica um conjunto definido de entradas e também mede um conjunto definido de saídas de modo a confirmar que algumas características do programa foram implementadas sem causar bugs. Isso é verdadeiro para qualquer tipo de teste que você deseje escrever.

Primeiramente, um test case deve se preocupar em validar apenas um aspecto do comportamento do programa e gerar um “pass” ou “fail”. Um teste pode falhar devido a um bug no produto, mas ele também pode falhar como resultado de um bug no teste propriamente dito — ou seja, um falso negativo. Da mesma forma, pode haver um falso positivo, ou seja, o teste pode passar devido a um bug no próprio teste e não porque o produto tenha se comportado de forma apropriada. Os bons testes devem obviamente evitar falsos negativos e nunca gerar falsos positivos. Além disso, um bom conjunto de testes não apenas testará cenários válidos como também casos de erro.

O objeto de teste e seu ambiente devem estar sempre no mesmo estado antes da execução do teste, a fim de que este possa ser repetido o número de vezes necessárias e oferecer resultados consistentes. Quando um test case falha, você deve receber informações adequadas sobre a localização da falha e seu motivo para que possa corrigir o problema, que é o motivo por que deseja que cada test case valide apenas uma coisa.

Agora, vamos aplicar essa teoria escrevendo alguns test cases simples para o exemplo do Quad. Abra o projeto Quad no Visual Studio e no NUnit da mesma maneira que o fez para o projeto Money — selecione File | Open e localize o arquivo QuadApp.csproj. Para adicionar ao projeto a referência ao nunit.framework, selecione Project | Add Reference, procure o arquivo nunit.framework.dll (localizado no diretório de instalação do NUnit dentro do subdiretório bin) e pressione OK.

Use o Visual Studio para criar a classe QuadTest: selecione Project | Add Class, digite o nome QuadTest.cs e pressione Open. Digite o código mostrado na Listagem 2 no QuadTest, crie-o (Build | Build QuadApp) e, em seguida, execute os testes usando NUnit (pressione Run). Clique na guia Console.Out no painel direito para ver a ordem da execução.

É possível ver agora como as várias funções Setup e TearDown podem ajudá-lo a controlar o ambiente de teste, permitindo que os test cases sejam repetidos quantas vezes forem necessárias e forneçam resultados consistentes. Essas funções ajudam você a garantir que a execução de um test case não afete outro test case (isolamento de test case). Uma boa prova do isolamento de teste consiste na capacidade de executar os test cases em qualquer ordem. Tente alterar a ordem de execução de seus test cases para confirmar o isolamento deles.

 

Listagem 2 Classe QuadTest

using System;

using NUnit.Framework;

 

namespace QuadApp

{

    [TestFixture]

    public class QuadTest

    {

        [TestFixtureSetUp]

        public void DoTestSuiteSetUp()

        {

            Console.WriteLine("TestSuiteSetUp");

        }

 

        [TestFixtureTearDown]

        public void DoTestSuiteTearDown()

        {

            Console.WriteLine("TestSuiteTearDown");

        }

 

        [TearDown]

        public void DoTestCaseTearDown()

        {

            Console.WriteLine("TestCaseTearDown");

        }

 

        [SetUp]

        public void DoTestCaseSetup()

        {

            Console.WriteLine("TestCaseSetup");

        }

 

        [Test]

        public void Area()

        {

            Quad q = new Quad();

 

            Assert.IsTrue( q.Area(2, 3) == 6);

 

            Console.WriteLine("Area");

        }

        [Test]

        public void Perimeter()

        {

            Quad q = new Quad();

 

            Assert.IsTrue( q.Perimeter(2, 3) == 10);

 

            Console.WriteLine("Perimeter");

        }

    }

}

Test Fixtures e Suítes

Um dispositivo de teste (test fixture) NUnit é um objeto compartilhado por um ou mais test cases interessados em inicializar o objeto de teste ou em fornecer a ele algum recurso. Em termos de NUnit, um dispositivo de teste (test fixture) é uma classe com o atributo [TestFixture] cujos métodos fornecem:

Ø      Funções [Test] que formam os vários test cases. Esses test cases devem ser operações atômicas e não podem apresentar dependências em outros testes.

Ø      Funções [SetUp] e [TearDown] necessárias para redefinir (reset) o ambiente entre os test cases. Elas são executadas individualmente uma vez, no início e ao final de cada teste.

Ø      Funções [TestFixtureSetUp] e [TestFixtureTearDown] exigidas para objetos compartilhados por test cases. Elas executadas individualmente uma vez, no início e ao final do dispositivo de teste (test fixture).

 

Os objetos de que você necessita para executar os testes podem ser criados como variáveis de instância da classe [TestFixture] (compartilhada por todos os test cases) ou como variáveis locais dos métodos (private para um test case individual), e é por isso que eles são descritos como um dispositivo de teste (test fixture). No entanto, você também pode pensar na [TestFixture] do NUnit como uma maneira de organizar um conjunto de testes, já que cada classe forma um ramo individual na treeview, abaixo do nome do projeto.

Você deverá agora ser capaz de começar a praticar o TDD, já que pode escrever conjuntos de testes para seus próprios projetos e usar o NUnit para executá-los. Vamos analisar a seguir alguns dos problemas que você poderá encontrar durante um trabalho de desenvolvimento mais sério.

 

Usando o TDD no mundo real

Uma das primeiras coisas a considerar quando você usa o TDD desde um projeto comercial é decidir como organizar seus programas de forma a poder separar facilmente o código de produção do código usado para testá-lo. Você deseja rodar seus testes de programação durante o desenvolvimento do produto e, ao mesmo tempo, ser capaz de removê-los facilmente quando quiser liberar o código para release.

Outro problema que você pode encontrar é a dificuldade de testar aplicativos GUI comandados por entradas em teclado e mouse.  Por exemplo, como você escreve um teste que simule um clique de usuário em uma lista dropdown e, em seguida, verifica se ela foi preenchida com uma determinada lista de nomes de países?

A resposta a essas questões reside na divisão do código em componentes apropriados que possam ser criados, testados e implantados separadamente. Por exemplo, em vez de criar o Quad como uma classe no mesmo arquivo executável da aplicação principal, ele poderia ter sido incluído em uma biblioteca separada (.dll). Isso teria nos permitido desenvolver tanto o programa de teste como o programa de domínio como executáveis separados (.exe) que compartilhassem uma biblioteca comum (.dll), a qual conteria o Quad. Observe que, se você estiver apenas criando uma biblioteca, o NUnit e seu conjunto de testes podem ser usados como interface, em vez de exigirem canais separados.

A idéia de manter o programa principal bastante simples e de colocar a complexidade do negócio em classes contidas em uma biblioteca também pode ajudar a solucionar o problema dos testes de aplicativos GUI. Uma das regras do TDD é que você não teste código de terceiros, por isso não há nenhum requisito para você testar as classes de framework da GUI, embora às vezes seja útil testar interfaces, particularmente quando estas forem um pouco excêntricas. Isso significa que você pode capturar o evento do usuário em uma classe que funcione e, em seguida, passá-lo para processamento na classe que está desenvolvendo. Novamente, você pode desmembrar o programa de teste e o programa de domínio em executáveis separados que compartilhem as bibliotecas comuns, responsáveis pela maior parte de seu esforço de desenvolvimento.

Para vermos como isso funciona na prática, usaremos o TDD para desenvolver uma lista combobox para um aplicativo de formulários simples.

 

Desenvolvendo um teste e criando uma biblioteca

Crie um novo aplicativo de console denominado CountryTest, selecionando File | New | Project no Visual Studio. Adicione ao projeto a referência a nunit.framework (da mesma forma que você fez ao escrever o primeiro test case). Crie uma classe CountryBoxTest: selecione Project | Add Class, digite o nome CountryBoxTest.cs e pressione Open. Digite o código mostrado na Listagem 3 no CountryBoxTest e tente criá-lo. A criação falhará porque a classe CountryList ainda não foi escrita.

 

Listagem 3 Classe CountryBoxTest

[TestFixture]

public class CountryBoxTest

{

    [Test]

    public void CheckContent()

    {

        CountryLib.CountryList list = new

            CountryLib.CountryList();

       

        Assert.AreEqual("UK", list.GetCountry(1));

        Assert.AreEqual("US", list.GetCountry(2));

        Assert.AreEqual("CH", list.GetCountry(3));

        Assert.AreEqual(null, list.GetCountry(4));

    }

}

 

Use o Visual Studio para criar uma nova biblioteca de classes (class library) denominada CountryLib, selecionando File | New | Project (adicione à solução existente). Crie a classe CountryList: selecione Project | Add Class, digite o nome CountryList.cs e pressione Open. Digite o código a seguir na CountryList e crie a biblioteca:

 

public class CountryList

{

    public String GetCountry(int No)

    {

        return null;

    }

}

 

Primeiramente, adicione ao projeto a referência a CountryLib.dll e crie novamente (rebuild) o aplicativo console, CountryTest. Desta vez ele funcionará porque CountryList foi escrita.

Abra o projeto CountryTest.csproj em NUnit ( File | Open) e execute o teste. Ele falha, já que CountryList.GetCountry retorna null. Alterne para o Visual Studio e, em CountryList, corrija GetCountry de modo que ele retorne os valores exigidos pelo teste — apenas adicione algumas strings inicializadas como variáveis de instância, conforme mostrado na Listagem 4. Faça o procedimento mais simples possível para colocar o programa em operação. Crie novamente (rebuild) a biblioteca CountryList, alterne para NUnit e, em seguida, repita o teste — ele deverá agora ser executado com êxito.

Reescreva os códigos que julgar necessários ao CountryList e repita o teste para verificar se não quebrou nenhuma linha. Você pode decidir que armazenar as strings em uma ArrayList tornará o código mais simples, fácil de compreender e evitará duplicações, mas pode também optar por adiar essa decisão por enquanto.

 

Listagem 4 CountryList após a refatoração

public class CountryList

{

    string one = "UK";

    string two = "US";

    string three = "CH";

 

    public string GetCountry(int No)

    {

        if ( No == 1 )

            return one;

        if ( No == 2 )

            return two;

        if ( No == 3 )

            return three;

        else

            return null;

    }

}

 

Criando a GUI

Use o Visual Studio para criar um novo aplicativo baseado em Windows denominado CountryApp, selecionando File | New | Project (adicione à solução existente). Inclua no projeto a referência à CountryList.dll. Em seguida, adicione uma combobox ao aplicativo (arraste uma a partir da toolbox para o formulário principal). Digite o código mostrado na Listagem 5 em CountryApp e crie (build) o aplicativo.

Você agora tem a organização básica de seu projeto que lhe permitirá criar o aplicativo de GUI, utilizando as técnicas de TDD para desenvolver sua biblioteca CountryList. Pense em como a CountryList poderia processar os eventos de interface de usuário (por exemplo, clique no mouse, teclado, etc.) de uma maneira que pudesse ser testada por CountryTest e usada por CountryApp. Provavelmente você encontrará uma solução muito melhor para a CountryList do que a apresentada aqui, e este é exatamente o ponto que desejamos enfatizar: o TDD (test-driven development) permite melhorar o design do projeto à medida que ele amadurece. Você não precisa produzir um design perfeito desde o início, o que é uma ótima notícia para os desenvolvedores.

 

Listagem 5 Classe CountryApp

private void InitializeComponent()

{

    ...

 

    CountryLib.CountryList list = new CountryLib.CountryList();

 

    String country;

    int index = 1;

 

    while ((country = list.GetCountry(index++)) != null)

       this.comboBox1.Items.Add(country);

 

    ...

 

}

 

Conclusão

  A maioria dos designs é criada de cima para baixo e consiste essencialmente em um exercício de compreensão e solução de problemas por meio da classificação de itens em categorias com base em características observáveis — em outras palavras, tentamos criar uma hierarquia de objetos que modelasse o domínio do problema. Por outro lado, o TDD é realizado de baixo para cima por meio da aplicação em seqüência de uma série de soluções simples a pequenos problemas que surgem no design. A reescrita assegura que o design convirja para uma boa solução, em vez se afastar dela. Se você acha que essa prática é radical, você está certo. O TDD poderá mudar a maneira como o software é desenvolvido nesta década, assim como as técnicas orientadas a objeto o fizeram na década passada.

  Uma observação sobre o NUnit: ao usar o Visual Studio para abrir alguns dos exemplos de testes predefinidos distribuídos com o NUnit, você poderá descobrir que a referência a nunit.framework.dll é mostrada como "not found"; se isso acontecer, procure na janela Solution Explorer, pasta References. Neste caso, é necessário apagar a referência existente (selecione o arquivo, clique com o botão direito nele | clique em Remove) e, em seguida, adicione-o novamente (selecione References, clique com o botão direito do mouse | Add Reference, localize o arquivo e pressione OK). O arquivo nunit.framework.dll pode ser encontrado no diretório de instalação do NUnit, no subdiretório bin.