2010-01-08

Testes

Não tenho tido tempo para acabar a série sobre excepções ou para fazer um post sobre sockets (estou a tratar disso :). Aqui fica um post (a “pedido” de um colega) que já tinha preparado há uns tempos…


Como é que testam o vosso código?

Testes manuais

Monta-se um num instante com umas text boxes e um botão para executar um método em particular. Ou então faz-se algo semelhante na linha de comandos. Depois, é correr isto, introduzir uns certos valores e confirmar os resultados. Se já existe um interface navega-se esse interface para executar uma determinada operação e confirma-se se foi produzido o efeito desejado.

Aspectos positivos:

  • Os testes são interactivos, por isso podemos testar várias coisas de uma vez só;

Aspectos negativos:

  • Para testar perde-se algum tempo;
  • Repetir os testes é demasiado tedioso;
  • Podemos enganar-nos ao correr o teste;
  • Não existe forma de “guardar” os testes para mais tarde confirmar que ainda funciona;

Esta estratégia é bastante rudimentar, e apresenta demasiados problemas para se provar usável em qualquer coisa séria.

Testes como código

Escreve-se um método main que corre as operações que queremos testar com inputs escolhidos especialmente para isso e que verifica se o output está correcto.

Aspectos positivos:

  • Podemos repetir os testes facilmente: basta correr novamente;
  • Temos a certeza que corremos sempre o teste sem nos enganarmos;
  • Podemos guardar este método para correr mais tarde;

Aspectos negativos:

  • Só podemos correr um teste de cada vez, ou então testar tudo, o que pode levar muito tempo;

Esta estratégia apresenta melhorias significativas em relação à primeira, mas pode tornar-se difícil de gerir quando temos várias coisas para testar. Por vezes queremos testar apenas se uma determinada funcionalidade funciona sem ter de testar tudo novamente. E outras vezes queremos testar várias coisas ao mesmo tempo.

Podemos evoluir esta estratégia para simplificar essas tarefas. Por exemplo, podemos colocar cada teste no seu método, e no main chamamos todos esses métodos. Depois podemos comentar/descomentar essas chamadas para escolher que testes queremos correr. Ou podemos fazer isto de uma forma mais engenhosa, adicionando melhores mecanismos para escolher os testes, ou mesmo para os detectar automaticamente... Eventualmente chegaríamos à estratégia 3.

Testes como testes

Usa-se um framework de testes. Escrevem-se cada teste no seu próprio método e identificam-se os testes com uma convenção definida pelo framework. O framework detecta os testes automaticamente e disponibiliza formas simples de escolher que testes correr.

Aspectos positivos:

  • Podemos repetir os testes facilmente;
  • Temos a certeza que corremos sempre o teste sem nos enganarmos;
  • Todos os testes estão guardados para correr mais tarde;
  • Podemos correr os testes que quisermos sem grande dificuldade;

Aspectos negativos:

  • É necessário aprender a usar um framework;

Esta estratégia tem todos os benefícios das anteriores. O único aspecto negativo não é muito mau porque é um custo que se paga apenas uma vez.

Eis alguns exemplos de frameworks de testes:

Existe uma lista na Wikipedia.

Qualquer IDE minimamente decente tem suporte para um ou outro framework. Para aqueles que não têm, a maioria dos frameworks proporciona test runners tanto para a linha de comandos como GUIs.

xUnit

A grande maioria dos frameworks de testes pertencem a uma família conhecida como xUnit. Estes são bastante semelhantes entre eles, diferindo apenas nas convenções usadas para identificar os testes e em questões próprias de cada linguagem. Todos os exemplos que apresentei acima fazem parte desta família. Aprendendo a usar um destes é fácil usar os outros.

Organização

Nota: nestes exemplos vou usar NUnit e C#. Como as ideias são essencialmente as mesmas e praticamente só mudam os nomes, é extremamente fácil converter para outro framework.

Nestes frameworks cada teste é um método que não recebe parâmetros e não devolve nada. Cada framework tem a sua forma de distinguir métodos que são testes de outros métodos. Alguns usam o nome do método (ex: ter o prefixo ou sufixo test) enquanto outros usam as funcionalidades para metadados das respectivas plataformas (ex: anotações em Java ou atributos em .NET).

Estes testes são agrupados em classes para formar test fixtures. A granularidade das test fixtures depende das preferências de cada um. Há quem use uma fixture por cada classe sob teste, e há quem use uma fixture por cada operação que pode ser efectuada.

Alguns frameworks permitem que os testes tenham metadados associados, permitindo, por exemplo classificar os testes por categorias.

Também é comum estes frameworks permitirem separar código que deve correr antes e depois de cada teste de uma fixture para executar código comum (criar objectos de teste, repor estado partilhado, etc).

Eis um exemplo de uma fixture que mostra isto tudo:

[TestFixture]
public class JustOneSimpleFixture
{
[SetUp]
public void SetUp()
{
// Isto corre antes de cada teste nesta fixture
}

[TearDown]
public void TearDown()
{
// Isto corre depois de cada teste nesta fixture
}

[Test]
public void JustOneSimpleTest()
{
//...
}

[Test, Category("Database")]
public void OneDatabaseTest()
{
//...
}
}

Triple A

Uma forma típica de estruturar um teste é dividí-lo em três fases: preparar (arrange), agir (act), verificar (assert). Este forma é conhecida como AAA (Arrange-Act-Assert).

Arrange

Na primeira fase, preparam-se os objectos que vão ser testados para estarem no estado esperado. Por exemplo, se estivermos a testar que um cliente não tem dívidas se pagou todas as facturas, temos de criar um cliente com as facturas todas pagas antes de testar.

Act

Na segunda fase, efectua-se a acção que vai ser testada. Como exemplo, se estivermos a testar que quando o cliente paga uma factura é emitido um recibo, temos de chamar o(s) método(s) que fazem com que uma factura seja paga.

Assert

Na terceira e última fase, verificam-se os resultados contra os esperados. Voltando ao exemplo de pagar uma factura, é nesta fase que verificamos se o recibo foi emitido. Os frameworks disponibilizam métodos especiais para fazer estas verificações, em inglês chamadas assertions.

Eis um exemplo de um hipotético teste escrito segundo esta forma:

[Test]
public void PayingAnInvoiceCreatesNewReceipt()
{
// Arrange
var p = new PaymentService();
var invoice = new Invoice();
Fill(invoice); // este método preenche a factura com dados fictícios
p.Invoices.Add(invoice);
int receiptsBefore = p.Receipts.Count; // contar os recibos existentes antes de pagar

// Act
p.PayInvoice(invoice);

// Assert
Assert.AreEqual(receiptsBefore + 1, p.Receipts.Count); // verificar que há mais um recibo do que antes
}

Quando corremos um teste com o test runner, sabemos se o teste passa ou não. Se o teste não passa temos uma explicação do que falhou.

Algo deste género:

Um teste que falhou

Parece que aquele teste falhou… Podemos ver porquê:

Porque é que o teste falhou

O resultado devia ser menor que zero mas foi 1. E depois de uns minutinhos a investigar e corrigir este pequeno bug:

Agora já passam todos

Conclusão

Usar um framework de testes permite-nos testar o código de uma forma mais simple e rápida, sem necessidade de criar interfaces apenas com esse propósito, ou qualquer outro tipo de malabarismos.

xUnit é uma família de frameworks de testes que cobre várias linguagens e plataformas, mas que usam os mesmos conceitos e a mesma estrutura.

No entanto, não basta escrever testes. É necessário saber o que testar e como. Mas isso fica para outro dia.

0 commentários:

Enviar um comentário