segunda-feira, 13 de julho de 2009

Utilizando o NUnit - Parte 2

Introdução


Neste post, continuarei a demonstrar alguns recursos da ferramenta de testes NUnit, conforme prometido na Parte 1.


Vimos como instalar e configurar um projeto de testes para ser executado no NUnit, através de um exemplo simples. Agora, nosso objetivo é explorar mais dois recursos interessantes:
  • Execução de testes que devem retornar uma Exception; e,
  • Configuração do Setup de uma classe de testes.

Execução de testes que devem retornar uma Exception

Imaginem o seguinte cenário: um usuário tenta cadastrar um novo Cliente, sem informar o RG. Como regra de negócio, o RG é uma informação obrigatória, portanto, ao tentar executar essa operação, o sistema deve retornar uma Exception.

O primeiro passo para implementarmos essa regra, é alterar a operação gravar da nossa classe Cliente para ficar assim:

23 public void gravar()
24 {
25 if (this.RG.Trim().Equals(""))
26 throw new ArgumentException("RG é uma informação obrigatória.");
27
28 // código para gravação do Cliente...
29 }

Reparem que, se o atributo RG não vier preenchido, uma nova Exception do tipo System.ArgumentException é disparada. Notem também que aqui poderíamos criar uma Exception customizada, mas foge do propósito deste post.

Agora, para conseguirmos testar esse comportamento no NUnit, vamos criar mais um caso de teste na nossa classe TestesCliente. O nome deste caso de teste será: gravarClienteSemInformarRG, onde criaremos um objeto Cliente, informaremos o Nome, não informaremos o RG e esperaremos como retorno a Exception, conforme implementada na classe Cliente. Segue o código do caso de teste:

21 [Test]
22 public void gravarClienteSemInformarRG()
23 {
24 Cliente.Cliente cliente = new Cliente.Cliente("Renato", "");
25 cliente.gravar();
26 }

Lembrando que, para que o teste apareça na IDE do NUnit, é preciso que o método esteja com o atributo [Test] configurado.

Agora, executando o método no NUnit, temos o seguinte resultado:

Figura 1: Execução do teste
gravarClienteSemInformarRG

O teste falhou. E qual a causa da falha? Simples: temos que dizer no método de teste que esperamos uma Exception! Para isso, basta incluir o atributo ExpectedException passando o nome da Exception esperada como parâmetro ("System.ArgumentException") no método de testes:

19 [Test]
20 [ExpectedException("System.ArgumentException")]
21 public void gravarClienteSemInformarRG()
22 {
23 Cliente.Cliente cliente = new Cliente.Cliente("Renato", "");
24 cliente.gravar();
25 }

Execute novamente e veja que o teste passa, pois o método gravar retorna uma Exception e é exatamente isso que queremos!

Configuração do Setup de uma classe de testes

Agora, suponhamos que eu quero criar um teste para pesquisar um cliente válido pelo seu nome. Para conseguirmos executar este teste com sucesso, precisamos que o Cliente já exista. Neste exemplo, não estou utilizando nenhum banco de dados para armazenar as informações, mas, se estivesse, seria necessário ter um registro válido no banco de dados para que o teste seja executado.

Como resolvemos isso? A primeira idéia seria criar o registro no banco de dados e executar o teste da pesquisa. Isso funciona, mas tem um problema: se alguém apagar o registro do banco de dados, o teste para de funcionar.

Para contornar isso, uma boa prática nestes cenários é criar uma massa de dados no início da execução de um teste e excluí-la no final. Ou seja, os testes devem ser consistentes, independente dos registros que já existirem na base de dados.

No NUnit isso é resolvido criando-se um método com o atributo Setup (que será executado no início da chamada de cada método de teste) e outro método com o atributo TearDown (que será executado no fim de cada método de teste). A seguir, demonstrarei o uso do Setup (o TearDown segue a mesma lógica).

No meu exemplo, criarei um método com o atributo Setup que será responsável por gravar um Cliente. Primeiro, criarei um atributo privado do tipo Cliente no escopo da classe de teste:

12 private Cliente.Cliente cliente;

Em seguida, criarei o método de Setup:

37 [SetUp]
38 public void Inicializar()
39 {
40 cliente = new Cliente.Cliente("Renato", "12345");
41 cliente.gravar();
42 }

Criarei agora o método pesquisarPorNomeValido na classe de teste, onde chamarei a operação pesquisarPorNome, da classe Cliente, passando no parâmetro nome a string "Renato", que foi o objeto criado no método Inicializar.

29 [Test]
30 public void pesquisarPorNomeValido()
31 {
32 cliente = cliente.pesquisarPorNome("Renato");
33
34 Assert.AreEqual("12345", cliente.RG);
35 }

Por fim, o método pesquisarPorNome, da classe Cliente, ficará assim:

37 public Cliente pesquisarPorNome(string nome)
38 {
39 if (nome.Equals(this.Nome))
40 return this;
41 else
42 return null;
43 }

Compilem o programa, executem o teste pesquisarPorNomeValido no NUnit e vejam que tudo funciona!

Explicando o código acima: no método Inicializar, coloquei o atributo Setup, que será executado sempre no início da chamada de todos os métodos de teste. Neste método, criei um novo objeto Cliente, de nome "Renato" e RG "12345". Reparem que não alterei minha classe "Cliente", logo o método gravar continua não fazendo nada (apenas validando se o RG foi informado). Neste ponto poderíamos, por exemplo, fazer a gravação dessas informações no banco de dados.

Após executar o método Inicializar, o método pesquisarPorNomeValido utiliza o mesmo objeto cliente (que foi criado pelo método de Setup), efetua a pesquisa pelo nome "Renato" e compara se o RG do cliente é igual a "12345". Como as strings conferem, então o teste é executado com sucesso.

Se no parâmetro nome for informado qualquer coisa diferente de "Renato", o teste falhará. O mesmo é válido se a comparação for feita com qualquer outra string diferente de "12345".

Abaixo segue o código fonte completo do exemplo.

Classe Cliente:

1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5
6 namespace Cliente
7 {
8 public class Cliente
9 {
10 public Cliente() : this(null, null)
11 {
12 }
13
14 public Cliente(string nome, string rg)
15 {
16 this.Nome = nome;
17 this.RG = rg;
18 }
19
20 public string Nome { get; set; }
21 public string RG { get; set; }
22
23 public void gravar()
24 {
25 if (this.RG.Trim().Equals(""))
26 throw new ArgumentException("RG é uma informação obrigatória.");
27
28 // código para gravação do Cliente...
29 }
30
31 public IList pesquisar()
32 {
33 // código para pesquisa de Clientes...
34 return null;
35 }
36
37 public Cliente pesquisarPorNome(string nome)
38 {
39 if (nome.Equals(this.Nome))
40 return this;
41 else
42 return null;
43 }
44
45 public void excluir()
46 {
47 // código para exclusão do Cliente...
48 }
49 }
50 }

Classe TestesCliente:

1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using NUnit.Framework;
6
7 namespace TestesCliente
8 {
9 [TestFixture]
10 public class TestesCliente
11 {
12 private Cliente.Cliente cliente;
13
14 [Test]
15 public void gravarCliente()
16 {
17 Cliente.Cliente cliente = new Cliente.Cliente("Renato", "12345");
18 cliente.gravar();
19 }
20
21 [Test]
22 [ExpectedException("System.ArgumentException")]
23 public void gravarClienteSemInformarRG()
24 {
25 Cliente.Cliente cliente = new Cliente.Cliente("Renato", "");
26 cliente.gravar();
27 }
28
29 [Test]
30 public void pesquisarPorNomeValido()
31 {
32 cliente.pesquisarPorNome("Renato");
33
34 Assert.AreEqual("12345", cliente.RG);
35 }
36
37 [SetUp]
38 public void Inicializar()
39 {
40 cliente = new Cliente.Cliente("Renato", "12345");
41 cliente.gravar();
42 }
43 }
44 }

Conclusão

Com esses recursos, conseguimos resolver entre 80 e 90% das situações de testes do cotidiano. Existem ainda alguns outros recursos que podem ser testados, como o próprio atributo TearDown, para executar rotinas após a execução dos métodos de teste (por exemplo, limpar os registros de teste da base de dados).

Até a próxima.

Nenhum comentário:

Postar um comentário