Twitter
RSS

Test-Driven Development no .NET (na prática)

0
No post anterior, fiz uma introdução ao Test-Driven Development (TDD), falando resumidamente sobre seus conceitos e mostrando como funciona o NUnit Testing Framework. Neste post, pretendo falar mais sobre a metodologia TDD e mostrar um exemplo de sua aplicação na prática.

Com esses conhecimentos já posso sair desenvolvendo programas utilizando TDD? Infelizmente não funciona bem assim, apenas sabendo programar orientado a objeto, conhecendo a sintaxe, atributos e métodos do NUnit não é suficiente. É preciso ter em mente algumas técnicas utilizadas em TDD antes de sair desenvolvendo um sistema. Existem algumas técnicas que devem ser seguidas quando se desenvolve utilizando TDD que facilitam o entendimento, no entanto, conhecer essas técnicas é apenas um começo. Para melhorar a sua experiência e aptidão, você deve praticar, práticar e práticar. Segue uma imagem para ilustrar melhor essas técnicas.

Se ainda não está familiarizado com TDD, o que estou escrevendo pode soar um pouco estranho. Muitas pessoas passaram muito tempo ouvindo que deve-se primeiro considerar cuidadosamente o design das classes, codificá-las e então testar. O que o TDD sugere é uma abordagem completamente diferente, o processo é iniciado de trás para frente, tendo o teste em primeiro lugar. Dito de outra forma, você não escreve uma linha de código até que você tenha um teste pronto para a função. A seqüência sugerida para desenvolvimento usando TDD é a seguinte:

1. Escrever um teste.
2. Execute o teste. Não consegue compilar uma vez que o código ainda não existe mesmo! (Esta é a mesma coisa que falhar.)
3. Escrever um esboço do que se deseja testar, só para fazer o teste compilar.
4. Execute o teste. Ele deve falhar. (Se não falhar , então o teste não era muito bom.)
5. Implementar o código completo para o teste passar.
6. Execute o teste. Ela deverá passar. (Se não o fizer, volte um passo e tente novamente.)
7. Começar com um novo teste!

Antes de chegar na etapa 5, você estará utilizando um processo denominado Codificação por Intenção. Quando você pratica Codificação por Intenção, o código é escrito de cima para baixo em vez de baixo para cima, ou seja, primeiro você cria um código de acesso a rotina, depois você cria a rotina em si. Em vez de pensar, "Eu preciso de uma classe com estes métodos" basta escrever o código desejado, antes que a classe realmente exista. Se tentar compilar seu código, não conseguirá, o compilador não poderá encontrar a classe. Este é um bom sinal, pois como disse acima, a falta de compilar, conta como uma falha no teste. O que vai fazer aqui é experimentar a intenção do código que está escrito. Isto não só ajuda a produzir códigos bem testados, como resulta também em código que é mais fácil de ler, mais fácil de depurar e com uma melhor concepção.

No desenvolvimento de software, testes tradicionais foram pensados para verificar se um trecho de código foi escrito corretamente. Quando você fizer TDD, no entanto, os seus testes são utilizados para definir o comportamento de uma classe antes de escrever a mesma. Não vou arriscar dizer que este método é mais fácil do que as velhas formas, mas na minha experiência é muito melhor.

Vamos ver uma pequena amostra desta metodologia. Suponhamos que você necessita escrever o simples e famoso sistema de conta bancária. Esse sistema consiste em fazer uma classe que possa representar uma Conta, possuindo o nome do proprietário, o saldo, e o estado da conta. Também pode ser feitos Depósitos, Saques e rendimento de juros. Tudo isso em um sistema simples num projeto Console Application. Veja a imagem abaixo que ilustra melhor o exemplo.

Antes de criar efetivamente a classe Conta, deve-se criar uma classe de teste, esses testes devem estar de acordo com as funcionalidades que imaginamos (“ou vemos na imagem acima”) que a classe Conta irá representar, com o desenvolvimento dessa classe iniciamos o TDD, o nome da classe de teste neste exemplo será ContaTests. Agora que já temos em mente o que a classe ContaTests deve ser capaz de fazer, vamos criar os testes com o NUnit Framework, conforme mostra o código abaixo:

using System;
using NUnit.Framework;

namespace TDD.Banco
{
    [TestFixture]
    public class ContaTests
    {
        [Test]
        public void TestDeposito()
        {
            Conta conta = new Conta("Test");
            conta.Deposito(125.0);
            conta.Deposito(25.0);
            Assert.AreEqual(150.0, conta.Balanco);
        }

        [Test]
        public void TestSaque()
        {
            Conta conta = new Conta("Test");
            conta.Saque(125.0);
            conta.Saque(25.0);
            Assert.AreEqual(-150.0, conta.Balanco);
        }

        [Test]
        public void TestJuro()
        {
            Conta conta = new Conta("Test");
            conta.Deposito(100.0);
            conta.AplicarJuro();
            Assert.AreEqual(105.0, conta.Balanco);
        }

        [Test]
        public void TestEstado()
        {
            Conta conta = new Conta("Test");
            conta.Deposito(1100.0);
            Assert.AreEqual("Ouro", conta.Estado);
            conta.Saque(600.0);
            Assert.AreEqual("Cinza", conta.Estado);
            conta.Saque(600.0);
            Assert.AreEqual("Vermelho", conta.Estado);
        }
    }
}

Depois de escrever o código acima na classe ContaTests é hora de compilá-la. É óbviu que a classe de testes não irá compilar, uma vez que a não existi a classe Conta. Isto ilustra o principal princípio de TDD, não escrever qualquer código sem que se tenha um teste pronto para ele. Lembro, quando os seus códigos de teste não compilarem, isso contará como uma falha de teste, e será cumprida a primeira etapa de TDD. Agora vamos criar a classe Conta com código suficiente apenas para fazer os testes compilar.

using System;

namespace TDD.Banco
{
    public class Conta
    {
        private String estado;       
        private String proprietario;

        public Conta(string proprietario)
        {           
            this.proprietario = proprietario;
            estado = "";           
        }

        public double Balanco
        {
            get { return 0.0; }
        }

        public String Estado
        {
            get { return estado; }
            set { estado = value; }
        }

        public void Deposito(double valor)
        {           
        }

        public void Saque(double valor)
        {           
        }

        public void AplicarJuro()
        {           
        }

        private void AlteraEstado()
        {           
        }
    }
}

Logo após criarmos a classe Conta, podemos compilar o projeto que não terá mais erros. Com isso podemos rodar nossos testes através do NUnit GUI. Para rodar os testes, rode o executável do NUnit GUI e abra o arquivo executável do projeto Console Application que foi criado nesse exemplo de teste. Clicando em “Run”, os testes serão executados e a tela do NUnit ficará conforme a imagem abaixo.

Notamos que todos os testes falharam. Isso já era esperado, pois a classe Conta foi criada apenas para compilar o projeto e os testes. Como pode ser visto na imagem, o teste “TestDeposito” mostrou a seguinte mensagem de erro: "TestDeposito: expected: <150> but was <0>". Esse resultado serviu para concluir mais uma etapa do TDD criar o teste de um método e fazer ele falhar. A próxima etapa é justamente implementar o método Deposito da classe Conta para que o teste “TestDeposito” passe sem erro.

Notém que por uma questão prática estou disponibilizando os códigos com todos os testes e métodos criados, quando se utiliza TDD essa não é uma boa prática, deve-se criar um teste para uma determinada rotina, criar a rotina apenas para compilar, rodar o teste propositalmente para que ele falhe, implementar a rotina para que o teste passe, tudo isso nessa respectiva ordem. Sabendo disso, vejam o código abaixo, com os testes implementados para que os testes passem.

using System;

namespace TDD.Banco
{
    public class Conta
    {
        private String estado;
        private double balanco;
        private String proprietario;

        public Conta(string proprietario)
        {
            // Novas contas por default são 'Cinza' e o balanco é 0.0
            this.proprietario = proprietario;
            estado = "Cinza";
            balanco = 0.0;
        }

        public double Balanco
        {
            get { return balanco; }
        }

        public String Estado
        {
            get { return estado; }
            set { estado = value; }
        }

        public void Deposito(double valor)
        {
            balanco += valor;
            AlteraEstado();
            Console.WriteLine("Depositado {0:C} --- ", valor);
            Console.WriteLine(" Balanço = {0:C}", Balanco);
            Console.WriteLine(" Estado  = {0}", Estado);
            Console.WriteLine("");
        }

        public void Saque(double valor)
        {
            balanco -= valor;
            AlteraEstado();
            Console.WriteLine("Sacado {0:C} --- ", valor);
            Console.WriteLine(" Balanço = {0:C}", Balanco);
            Console.WriteLine(" Estado  = {0}", Estado);
            Console.WriteLine("");
        }

        public void AplicarJuro()
        {
            if (!String.Equals(Estado, "Vermelho"))
            {
                balanco += 0.05 * balanco;
                Console.WriteLine("Juros Aplicados --- ");
                Console.WriteLine(" Balanco = {0:C}", Balanco);
                Console.WriteLine(" Estado  = {0}", Estado);
            }
            else
            {
                Console.WriteLine("Juro não pode ser aplicado pelo Estado da conta.");
                Console.WriteLine(" Balanco = {0:C}", Balanco);
                Console.WriteLine(" Estado  = {0}", Estado);
            }
        }

        private void AlteraEstado()
        {
            if (balanco < 0.0)
                Estado = "Vermelho";
            else if (balanco < 1000.0)
                Estado = "Cinza";
            else
                Estado = "Ouro";
        }
    }
}

Com o código acima já implementado, ao rodar os testes no NUnit, todos deverão passar como mostra a imagem abaixo. Tudo isso por que agora focamos no problema do teste, efetuamos a programação visando um objetivo, passar nos testes, por isso a importância de testar tudo que é possível.

Agora temos a classe Conta implementada e uma classe de testes ContaTest garantindo que todos os métodos estão funcionando de acordo com o previsto. Então podemos dizer que o TDD se encerra por aqui? ERRADO. Ainda falta uma etapa muito importante dentro do TDD, é a etapa do Refactoring.

Refatoração (do inglês Refactoring) é o processo de modificar um sistema de software para melhorar a estrutura interna do código sem alterar seu comportamento externo. (Wikipedia)

Devemos fazer um Refactoring tanto na nossa classe de teste quanto na nossa classe Conta. Vamos começar com a classe de teste, no código dela podemos notar que existe várias duplicações. Para cada método criamos e instanciamos um objeto do tipo Conta. Com ajuda dos atributos do NUnit, podemos refatorar essa classe excluindo essas repetições em cada método e substituindo esse código pelos método Setup e TearDown. Com isso cada vez que um teste for executado antes passará pelo Setup e depois pelo TearDown. Confira o código abaixo.

using System;
using NUnit.Framework;

namespace TDD.Banco
{
    [TestFixture]
    public class ContaTests
    {
        private Conta conta;

        [SetUp]
        public void Setup()
        {
            conta = new Conta("Test");
        }

        [TearDown]
        public void TearDown()
        {
            conta = null;
        }

        [Test]
        public void TestDeposito()
        {           
            conta.Deposito(125.0);
            conta.Deposito(25.0);
            Assert.AreEqual(150.0, conta.Balanco);
        }

        [Test]
        public void TestSaque()
        {           
            conta.Saque(125.0);
            conta.Saque(25.0);
            Assert.AreEqual(-150.0, conta.Balanco);
        }

        [Test]
        public void TestJuro()
        {           
            conta.Deposito(100.0);
            conta.AplicarJuro();
            Assert.AreEqual(105.0, conta.Balanco);
        }

        [Test]
        public void TestEstado()
        {           
            conta.Deposito(1100.0);
            Assert.AreEqual("Ouro", conta.Estado);
            conta.Saque(600.0);
            Assert.AreEqual("Cinza", conta.Estado);
            conta.Saque(600.0);
            Assert.AreEqual("Vermelho", conta.Estado);
        }
    }
}

Após a refatoração, podemos rodar os testes e conferir que todos continuam funcionando. Então vamos partir para um refactoring na classe Conta. Ao implementar a classe conta podemos constatar que de acordo com o estado da conta é tomada uma decisão diferente para cada caso. Isso impacta em várias verificações em cada rotina que se aplica uma operação que necessita uma tomada de decisão de acordo com o estado. Portanto para um melhor design da classe e para facilitar possíveis alterações duranto o tempo de vida do projeto vamos aplicar a classe Conta o Desing Pattern State, conforme imagem abaixo.

Conforme podemos ver no modelo acima, o design da classe vai mudar, porém o objetivo da classe Conta vai continuar sendo o mesmo, permitindo fazer deposito, saque e rendimentos de juro. Neste caso vamos supor que outro desenvolvedor resolveu fazer essa mudança, ele não vai seguir as metodologias TDD, não vai criar testes para as novas implementações, vai apenas alterar o design. Com isso podemos ver se o sistema está mais seguro com os testes que foram implementados. Vejam o código abaixo após o refactoring.

Classe Conta:

using System;

namespace TDD.Banco
{
    public class Conta
    {
        private Estado estado;       
        private String proprietario;

        public Conta(string proprietario)
        {
            // Novas cotas por default são 'Cinza' e tem o balanco 0.0
            this.proprietario = proprietario;
            estado = new EstadoCinza(0.0, this);           
        }

        public double Balanco
        {
            get { return estado.Balanco; }
        }

        public Estado Estado
        {
            get { return estado; }
            set { estado = value; }
        }

        public void Deposito(double valor)
        {
            estado.Deposito(valor);
            Console.WriteLine("Depositado {0:C} --- ", valor);
            Console.WriteLine(" Balanço = {0:C}", Balanco);
            Console.WriteLine(" Estado  = {0}", Estado.GetType().Name);
            Console.WriteLine("");
        }

        public void Saque(double valor)
        {
            estado.Saque(valor);
            Console.WriteLine("Sacado {0:C} --- ", valor);
            Console.WriteLine(" Balanço = {0:C}", Balanco);
            Console.WriteLine(" Estado  = {0}", Estado.GetType().Name);
            Console.WriteLine("");
        }

        public void AplicarJuro()
        {
            estado.AplicarJuro();      
            Console.WriteLine("Juros Aplicados --- ");
            Console.WriteLine(" Balanco = {0:C}", Balanco);
            Console.WriteLine(" Estado  = {0}", Estado.GetType().Name);
            Console.WriteLine("");
        }
    }
}

Classe Estado:

using System;

namespace TDD.Banco
{
    public abstract class Estado
    {
        protected Conta conta;
        protected double balanco;

        protected double juro;
        protected double limiteMin;
        protected double limiteMax;
       
        public Conta Conta
        {
            get { return conta; }
            set { conta = value; }
        }

        public double Balanco
        {
            get { return balanco; }
            set { balanco = value; }
        }

        public abstract void Deposito(double valor);
        public abstract void Saque(double valor);
        public abstract void AplicarJuro();
    }
}

Classe EstadoVermelho:

using System;

namespace TDD.Banco
{
    public class EstadoVermelho : Estado
    {
        double taxaServico;
       
        public EstadoVermelho(Estado estado)
        {
            this.balanco = estado.Balanco;
            this.conta = estado.Conta;
            Initialize();
        }

        private void Initialize()
        {           
            juro = 0.0;
            limiteMin = -100.0;
            limiteMax = 0.0;
            taxaServico = 15.00;
        }

        public override void Deposito(double valor)
        {
            balanco += valor;
            AlteraEstado();
        }

        public override void Saque(double valor)
        {
            valor = valor - taxaServico;
            Console.WriteLine("Sem saldo para efetuar saque!");
        }

        public override void AplicarJuro()
        {
            // Não tem juro estado vermelho
        }

        private void AlteraEstado()
        {
            if (balanco > limiteMax)
            {
                conta.Estado = new EstadoCinza(this);
            }
        }

        public override String ToString()
        {
            return "Vermelho";
        }
    }
}

Classe EstadoCinza:

using System;

namespace TDD.Banco
{
    public class EstadoCinza : Estado
    {
        public EstadoCinza(Estado estado) :
            this( estado.Balanco, estado.Conta)
        {   
        }

        public EstadoCinza(double balanco, Conta conta)
        {
            this.balanco = balanco;
            this.conta = conta;
            Initialize();
        }       

        private void Initialize()
        {
            juro = 0.05;
            limiteMin = 0.0;
            limiteMax = 1000.0;           
        }

        public override void Deposito(double valor)
        {
            balanco += valor;
            AlteraEstado();
        }

        public override void Saque(double valor)
        {
            balanco -= valor;
            AlteraEstado();
        }

        public override void AplicarJuro()
        {
            balanco += juro * balanco;
            AlteraEstado();
        }

        private void AlteraEstado()
        {
            if (balanco < estado =" new"> limiteMax)
            {
                conta.Estado = new EstadoOuro(this);
            }
        }

        public override String ToString()
        {
            return "Cinza";
        }
    }
}

Classe EstadoOuro:

using System;

namespace TDD.Banco
{
    public class EstadoOuro : Estado
    {
        public EstadoOuro(Estado estado) :
            this(estado.Balanco, estado.Conta)
        {
        }

        public EstadoOuro(double balanco, Conta conta)
        {
            this.balanco = balanco;
            this.conta = conta;
            Initialize();
        }

        private void Initialize()
        {
            juro = 0.05;
            limiteMin = 1000.0;
            limiteMax = 1000000.0;
        }

        public override void Deposito(double valor)
        {
            balanco += valor;
            AlteraEstado();
        }

        public override void Saque(double valor)
        {
            balanco -= valor;
            AlteraEstado();
        }

        public override void AplicarJuro()
        {
            balanco += juro * balanco;
            AlteraEstado();
        }

        private void AlteraEstado()
        {
            if (balanco < 0.0)
            {
                conta.Estado = new EstadoVermelho(this);
            }
            else if (balanco < limiteMin)
            {
                conta.Estado = new EstadoCinza(this);
            }
        }

        public override String ToString()
        {
            return "Ouro";
        }
    }
}

Após terminarmos as implementações, podemos rodar os testes no NUnit e verificar se algum deles falhou, indicando que alguma alteração mecheu no funcionamento do sistema. Neste exemplo foi deixado propositalmente duas alterações para que fossem pegas pelos testes. Como mostra a imagem do NUnit abaixo.

Dois testes falharam durante a execução, o primeiro deles foi o “TestEstado” onde esperava uma string “Ouro” e veio como retorno um tipo Ouro. No segundo teste que falhou, o “TestSaque” era esperado -150 no balanco e retornou -125, isso devido a uma alteração na lógica da conta com estado Vermelho, só pode sacar se tiver fundos. Vamos corrigir os testes, para que eles fiquem adequados as novas alterações.

using System;
using NUnit.Framework;

namespace TDD.Banco
{
    [TestFixture]
    public class ContaTests
    {
        private Conta conta;

        [SetUp]
        public void Setup()
        {
            conta = new Conta("Test");
        }

        [TearDown]
        public void TearDown()
        {
            conta = null;
        }

        [Test]
        public void TestDeposito()
        {           
            conta.Deposito(125.0);
            conta.Deposito(25.0);
            Assert.AreEqual(150.0, conta.Balanco);
        }

        [Test]
        public void TestSaque()
        {
            conta.Deposito(200.0);
            conta.Saque(125.0);
            conta.Saque(25.0);
            Assert.AreEqual(50.0, conta.Balanco);
        }

        [Test]
        public void TestSaqueVermelho()
        {           
            conta.Saque(125.0);
            conta.Saque(25.0);
            Assert.AreEqual(-125.0, conta.Balanco);
        }

        [Test]
        public void TestJuro()
        {           
            conta.Deposito(100.0);
            conta.AplicarJuro();
            Assert.AreEqual(105.0, conta.Balanco);
        }

        [Test]
        public void TestEstado()
        {           
            conta.Deposito(1100.0);
            Assert.AreEqual("Ouro", conta.Estado.ToString());
            conta.Saque(600.0);
            Assert.AreEqual("Cinza", conta.Estado.ToString());
            conta.Saque(600.0);
            Assert.AreEqual("Vermelho", conta.Estado.ToString());
        }
    }
}

Uma dica importante quanto à metodologia de TDD é a seguinte: toda vez que você encontrar um bug, primeiramente crie um teste que faça o bug aparecer. Só então, escreva o código que vai corrigir o bug. Com isso, o seu conjunto de testes melhora com o tempo, fica mais completo, e você vai ter certeza que daquele ponto em diante que o bug nunca mais voltará despercebido. É o que foi feito no caso do teste “TestSaque”, foi criado um teste “TestSaqueVermelho” apenas para simular o erro e depois corrigi-lo.

Podem acreditar, depois de usar NUnit por um tempo, vocês vão ver a importância dos testes, principalmente depois de fazer grandes alterações no sistema: se todos os testes passarem, você pode ter certeza que não quebrou nada, ao invés de só rezar. Também vai ter certeza que não danificou funcionalidades implementadas por outros programadores, o que em um ambiente de produção é fundamental. Você se sente mais seguro para fazer alterações, por que sabe que possívelmente pegará os erros que podem vir a ocorrer.

Como puderam notar a classe Conta com o novo design acabou ficando dependente de outras classes como EstadoVermelho, EstadoCinza e EstadoOuro. Testes de unidade procuram testar classes de um sistema isoladamente. Classes em um sistema normalmente alcançam seus objetivos com a ajuda de outras. Não funcionam isoladamente, freqüentemente se comunicam com outros elementos da aplicação. Quando construímos um teste de unidade, um dos principais desafios é exatamente isolar a classe que está sendo testada, para que nenhuma outra classe do sistema seja envolvida no teste.

Uma solução eficaz é o uso de mock objects (objetos “de mentira” ou objetos substitutos), que permitem isolar as classes de um sistema de forma bastante simples. No nosso exemplo, a classe Conta depende diretamente da classe EstadoVermelho. Usar um mock object significa que, quando estivermos testando, ao invés de usarmos a classe EstadoVermelho, usaremos uma outra, que “finge” ser essa classe, mas é mais simples e mais fácil de ser usada durante os testes (além disso, temos total controle sobre ela, pois é criada especialmente para os testes).

Mas isso pretendo mostrar em um próximo post, refatornado a nossa classe de teste, permitindo o uso de mock objects e criando novas classes de testes para cada classe separadamente.

Até a próxima…


Test-Driven Development no .NET

4
Esse post apresenta a técnica de desenvolvimento orientado a testes, que tem como um de seus objetivos antecipar a identificação e correção de falhas durante o desenvolvimento. Test-driven development ou TDD é um novo paradigma("adoro essa palavra") de desenvolvimento onde primeiro são criados os testes e somente depois é escrito o código necessário para passar por eles. Muita gente deve estar se perguntando: "Mas como vou fazer os testes se não tenho o código para testar?". Vamos tentar esclarecer essas dúvidas no decorrer desse post.

Antes de inicar o desenvolvimento utilizando TDD, deve-se ter o entendimento do que é Unit Test. Em termos práticos, Unit Test (Testes Unitários), significa escrever trechos de código que provem verdadeiro todas as categorias em sua aplicação, ou seja, testar se o objetivo do método está sendo atendido, verificar se a rotina não está sensível a um bug. São apenas testes que garantem que os métodos executam sem erros o que se espera deles.

Embora muitos dos desenvolvedores façam testes unitários em seus códigos, geralmente os testes são desenvolvidos/realizados após o código ser concebido e escrito. Como um grande número de desenvolvedores escreve os códigos antes dos testes, os testes acabam sendo viciosos e difícil de fazer muitas variações devido ao curto tempo. Desenvolvimento orientado a testes (TDD) tenta resolver este problema para gerar maior qualidade no código, evitando que se coloque a carroça na frente dos bois, redigindo os testes antes que escrever o código.

Uma das práticas de Extreme Programming (XP), TDD está adquirindo uma forte comunidade em Java, mas muito pouco foi escrito sobre como fazer em .NET. Ainda assim, foi desenvolvido para .NET, o NUnit Testing Framework, um framework de testes baseado no JUnit da comunidade Java, que serve para executar e apresentar o resultado dos testes. Como ele faz isso? O Test Runner do NUnit faz uma busca no código compilado, procurando por atributos que identifiquem as classes e métodos como de teste. Em seguida, através do uso do Reflection, ele executa essas classes e métodos que foram considerados como de testes. Para informar que a classe é uma classe de testes, não é preciso que ela derive de uma classe base de testes, somente é necessário que sejam utilizados os atributos. O NUnit, possui uma variedade de atributos que devem ser utilizados em seus testes unitários. Eles são utilizados para definir test fixtures, test methods, setup e teardown. Veremos para que servem cada um deles logo abaixo.

Obs.: A forma mais prática de instalar NUnit no seu sistema é instalando o pacote Test Driven.Net, da versão mais recente do NUnit, instalará um add-in pro Visual Studio.Net para que você possa executar os testes dentro do ambiente, diretamente. Outra alternativa (por exemplo pra quem não usa VS.Net) é instalar apenas o NUnit executá-lo como um programa independente.

TestFixture

O atributo TestFixture é usado para indicar que a classe contém métodos de teste. Quando esse atributo é incluído na classe em seu projeto, o Test Runner irá procurar por métodos de teste para então executá-los. O código abaixo mostra o uso desse atributo.

namespace ExemploNUnitTest
{
  using System;
  using NUnit.Framework;

  [TestFixture]
  public class ClasseDeTeste
  {
  }
}

A restrição para classes que possuem o atributo TestFixture é que deve ter defino um construtor padrão como public.(Ou nenhum construtor definido o que é a mesma coisa).

Test

O atributo Test é utilizado para indicar que o método é um método de teste e deve ser executado pelo Test Runner. O método deve ser público, não deve retornar nada e não pode receber parâmetros. Caso contrário o teste não será executado. O código abaixo mostra o exemplo desse atributo.

namespace ExemploNUnitTest
{
  using System;
  using NUnit.Framework;

  [TestFixture]
  public class ClasseDeTeste
  {
    [Test]
    public void TesteUm()
    {
      // Faz alguma coisa...
    }
  }
}

Setup & TearDown

Muitas vezes é necessário inicializar ou limpar variáveis antes de começar a execução dos testes, poderia fazer isso criando um método privado com essas rotinas e chamá-los em cada método de teste. Mas, para isso existem os atributos SetUp e TearDown que indicam que um método deverá ser executado antes(SetUp) ou depois(TearDown) de cada método de teste. A utilização mais comum desses atributos é quando é necessário criar objetos dependentes (e.g., database connections, etc). Este exemplo mostra o uso desses atributos.

namespace ExemploNUnitTest
{
  using System;
  using NUnit.Framework;

  [TestFixture]
  public class ClasseDeTeste
  {
    private int _valor;

    [SetUp]
    public void Setup()
    {
      _valor = 5;
    }

    [TearDown]
    public void TearDown()
    {
      _valor = 0;
    }

    [Test]
    public void TesteUm()
    {
       // Faz alguma coisa...
    }
  }
}

Assert Class

Além dos atributos, um importante recurso que o NUnit disponibiliza é a classe Assert. Essa classe oferece uma variedade de métodos estáticos que podem ser usados para verificar se o resultado da execução do método está de acordo com o esperado. A seguir veja um exemplo do uso da classe Assert.

namespace ExemploNUnitTest
{
  using System;
  using NUnit.Framework;

  [TestFixture]
  public class ClasseDeTeste
  {
    [Test]
    public void TesteUm()
    {
      int i = 4;
      Assert.AreEquals( 4, i );
    }
  }
}

O resultado do método de teste do código acima seria Verdadeiro, caso o valor de i fosse diferente de 4 o método seria validado como Falso.

Rodando seus Testes

É muito simples rodar seus testes com o NUnit, ele possui duas aplicações diferentes para execução dos testes, a aplicação Windows GUI e a aplicação console XML. Para rodar os testes com a aplicação Windows GUI, basta rodar a aplicação, abrir o assembly onde residem seus testes e clicar no botão "Run". Veja um exemplo da aplicação Windows GUI na imagem abaixo:

A interface do NUnit é bem simples, ela nos mostra todos os testes, com o seguinte código de cores ao lado de cada um: vermelho se o teste não passou, e verde se passou. Uma barra de progresso à direita dá um resultado geral (ou seja, verde só se todos passaram).

Os testes ainda podem ser executados pela aplicação console através de linhas de comando. Ela também gera um XML com os resultados do teste, possibilitando formatá-los com XSLT e CSS para integra-los no Visual Studio. Porém essa aplicação é mais utilizada para Integração Contínua.

Vou terminar esse post por aqui para não ficar muito extenso, com esse conteúdo já da para ir treinando seus testes com o NUnit. No próximo post pretendo ir mais fundo em TDD, mostrar seu exemplo na prática e algumas dicas de metodologia no desenvolvimento.

Dica de Livro: Test-Driven Development by Example;

Até a próxima...