Palestra UniBH: Desenvolvimento Dirigido por Testes

A UniBH (Universidade de Belo Horizonte) me convidou no ano passado para falar sobre teste de software na Jornada Tecnologica,  e eu ministrei a palestra “Introdução a automação de testes“, comentada aqui no blog. Também fui convidado para esse ano, com o objetivo de abordar algo relacionado a teste de software, mas não diretamente sobre teste. Optei por falar sobre desenvolvimento dirigido por testes, já que percebi muito interesse das pessoas depois que publiquei os posts “Uma Introdução a TDD com JUnit” e principalmente “Testes de unidade para quem não sabe nada de programação“.

A UniBH novamente foi extremamente organizada. Contava com diversas palestras em várias salas e com uma recepção de primeira qualidade. Os alunos foram participativos e contei com a presença do Eduardo Habib, Test Manager da empresa Synergia, mestre em Ciência da Computação pela UFMG e professor dos cursos de computação da UniBH, uma das lendas da nossa área que ainda habitam Belo Horizonte.

A palestra iniciou com os conceitos de teste de unidade e desenvolvimento difirigo por testes seguiida de uma abordagem bem prática, através da demonstração de exemplos e criação de testes na hora.

Abaixo os slides e comentários:

Comentários: (click nas imagens para aumentar).

Inicialmente discutimos sobre o que é Teste de Unidade, o protagonista do TDD. Alguns alunos conheciam TDD, mas nenhuma já havia praticado. Mesmo sendo em uma universidade, sabemos que na nossa área é muito comum que o exercício da profissão não dependa exclusivamente de formação acadêmica e muitos dos alunos presentes atuavam em grandes, pequenas e médias empresas aqui de BH, com Java, .Net entre outras linguagens de programação, mas nenhum aplicava TDD.

Essa não é uma surpresa. O mercado está amadurecendo e isso faz com que as empresas tentam a “conter custos” como TDD, testes funcionais, automação de testes, revisões ou mesmo integração continua. É uma pena que todos esses investimentos ainda sejam vistos como despesas e que a qualidade não seja a prioridade . . . Mas enfim, muitos desses alunos gostaram do que viram, entenderam que não é complicado e que nem sempre representa um custo a mais no projeto, mesmo com visão imediatista.

Vimos o famoso modelo V de desenvolvimento de software e o problema que existe em segui-lo ao pé da letra, mantendo o teste de unidade depois da codificação. Quando falamos de testes funcionais, com um profissional de teste de software, capacitado e aplicando técnicas para execução de testes efetivos, os testes depois do desenvolvimento são muito válidos, mas quando falamos do desenvolvedor não.

Isso por um motivo muito simples. O desenvolvedor, quando escreve testes de unidade depois do código de produção, escreve testes para passar (inconscientemente). Ele escreve testes que atendem aos requisitos que ele implementou. São testes sem valor para a funcionalidade, já que se a funcionalidade estiver errada, os testes provavelmente estarão também.

O TDD veio para, entre outros objetivos, resolver este problema. Quando falamos de escrever testes antes do desenvolvimento do código de produção, falamos muito mais do que simplesmente antecipar testes. Falamos sobre estudar o que vamos desenvolver, sobre enxergar situações distintas que nossa lógica precisa atender, falamos sobre desenvolver com mais confiança e agilidade e falamos sobre escrever testes para testar e não simplesmente para ter testes.

Charles Fortes citou uma frase muito interessante do Giovanni Bassi na sua palestra sobre TDD: “Testes escritos antes do código da aplicação, antes de serem testes, são especificações”. É um ótimo ponto de vista pois é uma forma de tornar os requisitos menos abstratos.

*O modelo V aplicado ao TDD ilustrado acima é apenas uma adaptação do modelo original, não sendo objeto de estudo ou conceito aceito universalmente.

Para o teste de unidade, sempre precisamos de usar um framework, um repositório para dados que serão usados no teste (pode ser no próprio código fonte) e um requisito que será testado.

O teste de unidade normalmente usa a mesma linguagem de programação que estamos testando, e é desenvolvida na forma de um framework, normalmente mantido pela comunidade, que tem integração com as ferramentas usadas para o desenvolvimento.

Ressaltamos alguns aspectos importantes, como a diferença dos testes de unidade para os testes de integração, a importância dos testes de unidade serem executáveis independentes de bancos de dados e possuir todos os recursos que necessitam encapsulados neles, além de itens que já debatemos anteriormente, como o uso desses testes para regressão e seu baixo valor quando feio posteriormente ao código de produção.

Introduzimos algumas ‘regras’ também, como a não utilização de condições ou lógica de qualquer tipo dos testes, uma vez que isso pode gerar mais defeitos ou até mesmo falsos positivos, pois sabemos que a lógica implementada no código de produção provavelmente será a mesma dos testes.

Vimos que só editamos ou excluímos um teste quando esse não é mais válido de acordo com os requisitos do cliente, se precisarmos de um novo teste, mesmo que parecido, temos que criar um novo ao invés de editar outro teste.

Entendemos que temos várias ferramentas para falar sobre cobertura dos testes de unidade, mas que “bagunçar” o código de produção depois que todos os testes passarem, pode ser o melhor indicador de sucesso e cobertura dos nossos testes ou um grave alerta de ausência de cobertura.

Discutimos sobre testar apenas o que está exposto como serviço da unidade, afinal de contas, o que não está no contrato da unidade é usado somente pela unidade, logo não precisa de testes específicos e suas situações devem ser testadas nos serviços do contrato atual.

Vimos também algo que vamos falar mais aqui, o reuso do teste.

Aqui falamos sobre o processo de criação e realizamos uma pequena demonstração no Visual Studio 2010. Para mais detalhes sobre a aplicação do TDD, consultar “Uma Introdução a TDD com JUnit“.

Vimos também que temos elementos do teste de software como aplicações de técnicas, habilidades necessárias e ligação com os requisitos e temos elementos de desenvolvimento como orientação por objeto, padrões de projeto, acesso a dados externos e quaisquer recursos da linguagem de programação usada.

Entendemos a anatomia de um teste de unidade e um princípio muito importante: Testes não possuem lógica (ifs, cases, etc), testes são configurados.

Vimos que assim como o caso de teste, o teste de unidade é composto por três itens básicos:

Pré-Condições: Nos casos de teste a pré-condição representa passos anteriores ao teste em que preparamos o ambiente, dados e contexto do teste que iremos executar. No teste de unidade também temos que preparar o ambiente, os dados e o contexto que o teste será executado. Podemos dizer que nesse momento configuramos o nosso teste, inicializando valores, preparando objetos, acessando dados, definindo escopo ou qualquer outro item que seja necessário para o teste. Podemos dizer que aqui configuramos o testes.

Passos ou procedimentos de Teste: Nos casos de teste, os procedimentos são o teste propriamente dito, é neles que atribuímos valores, setamos comandos, executamos ações complementares, realizamos comparações intermediárias e demais itens relacionados a ações de teste. Nos testes de unidade nós realizamos as mesmas tarefas, usando os itens preparados anteriormente, passamos valores, criamos objetos que testaremos e executamos métodos. Podemos dizer que aqui configuramos o que será testado.

Pós condições ou resultados esperados: Nos casos de teste este é o momento em que verificamos se o que esperávamos como resultado foi realmente o que o sistema nos retornou. Dessa mesma forma realizamos uma comparação entre o resultado esperado e o resultado atual nos testes de unidade.

Vimos que tanto Java quanto as linguagens presentes no .Net tem dezenas de frameworks para testes de unidade, assim como PHP, Python, Ruby e outras mais de 70 linguagens de programação. Isso reforça como os testes de unidade são presentes e importantes, a maioria é disponibilizada abertamente pela comunidade e pode ser baixada sem custos adicionais.

Comentamos também que a decisão por um ou outro framework depende de diversos aspectos envolvidos na equipe, podendo inclusive misturar frameworks para facilitar a execução em alguns casos.

Para saber mais sobre esses frameworks pode acessar: http://en.wikipedia.org/wiki/List_of_unit_testing_frameworks.

Vimos dois exemplos de ‘Hello Unit-Test World”, um em Java usando JUnit e um em C# usando Microsoft Unit Test Framework. Como podemos ver, excluindo a sintax temos o mesmo código fonte. Teste de unidade é uma técnica e TDD é um conceito. Dominá-los em uma linguagem faz com que você esteja “quase preparado” para aplicá-los em qualquer outra linguagem ou framework do mercado.

Vimos alguns mitos do TDD, entre eles destaco alguns abaixo:

TDD é o suficiente, manda os testers embora”: O TDD é um filtro e uma boa prática de programação. Sua utilização sem dúvidas é uma ótima medida para melhorar a qualidade do projeto e reduzir consideravelmente os defeitos de um projeto, além de agilizar a entrega das funcionalidades, remover a maioria dos defeitos básicos e dúvidas do entendimento. Embora tenha todos esses benefícios, um profissional focado em testes funcionais de sistema é essencial para qualquer projeto de desenvolvimento de software. Muitos defeitos vão passar pelos testes de unidade e na ausência de um profissional de teste de software, o cliente vai ter um encontro muito desagradável com esses defeitos.

TDD é do analista de teste: Não. O Teste de unidade em uma abordagem de TDD sempre deve ser feito pelo profissional que desenvolverá o código de produção. Claro que nada impede o a participação do profissional de teste de software, de outro desenvolvedor ou até mesmo do cliente, mas uma das razões para os testes virem antes do código de produção é que esses testes irão ajudar os profissionais de desenvolvimento a entender melhor a funcionalidade que será desenvolvida.

TDD só é aplicável em projetos que usam XP, Scrum ou alguma coisa ágil, nunca pra processos tradicionais ou processos sem metodologias: Qualquer um pode usar TDD. Se estiver melhorando o que seu cliente vai receber, você pode usar até uniforme do Bob Esponja, o objetivo é entregar valor e não discutir o purismo da sua organização. Seu cliente vai ficar muito mais feliz em receber um projeto com mais qualidade do que se você falar com ele que segue o processo A ou tem o selo B.

Testes de unidade são testes de caixa-branca em uma única unidade e independem de outras unidades, interfaces gráficas / prompt de comandos ou dados para funcionar. Testes funcionais automatizados são testes de caixa-preta em um conjunto de funcionalidades do qual você não precisa saber nem ao mesmo qual a linguagem de programação foi usada. Embora compartilhem muitos conceitos e técnicas, um não relação direta com o outro e definitivamente não são a mesma coisa.

O TDD assim como o teste de software é um investimento, e como todo investimento ele tende a ter um gráfico com início nas alturas e término próximo a base. Você pode escolher gastar mais no início e manter o projeto mais controlado ou economizar na hora mais crítica e contar com a sorte no momento em que investimentos não terão resultados tão rapidamente.

TDD não é escalável: Tenha testes claros, preferencialmente dirigidos por dados e documentados corretamente. Isso porque se o seu projeto evoluir e seus testes não evoluírem, você desperdiçou dinheiro. Assim como casos de teste, casos de uso, histórias de usuários, diagramas e qualquer outra documentação, os testes de unidade devem evoluir junto com o projeto. E assim como esses artefatos comentados anteriormente, evoluir testes de unidade também não é uma tarefa fácil, principalmente quando o projeto está atrasado. Pensar em testes com manutentabilidade desde o começo é uma boa estratégia.

TDD para 100% do código fonte: Aqui mora uma discussão de anos que eu não pretendo resolver neste post :p . Algumas linhas do agile, como o XP pesam que todas as linhas de código estejam cobertas e que tenha sido por pelo menos um teste de unidade. O problema aqui é se preocupar mais com a cobertura do código do que com a cobertura dos testes. O que quero dizer é que pensarem código não é a melhor maneira de pensar em valor para o cliente. O cliente não está “nem aí” se você tem 100 ou 100000 de linhas de código no seu projeto, ele quer saber se a tela de cadastro funciona e se o cálculo do pagamento dos funcionários está calculando direito. Por isso acho mais interessante uma abordagem do tipo cobertura de todas as funcionalidades do ponto de vista do teste do que cobrir o código fonte. Mas cada um possui sua abordagem nesse assunto e se Kent Beck diz que temos que cobrir todo o código não sou eu quem vai discordar dele :), mas provavelmente essa não é a melhor abordagem para quem está começando.

A parte prática focou em mostrar diversos aspectos dos unit tests no visual studio

O objetivo dos testes, pra variar, era o nosso triangulo de Myers, que resultou na classe “Triangulo.cs“, que pode ser vista abaixo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace UniBhTirangle
{
public class Triangulo
{

public string retornaTipo(int lado1, int lado2, int lado3)
{
if ((lado1 == lado2)(lado2 == lado3)) return "Equilátero";

if (confereTriangulo(lado1,lado2,lado3)) return "Inválido1";

if ((lado1 == lado2)  (lado2 != lado3)) return "Isósceles";

if ((lado1 != lado2)  (lado2 != lado3)) return "Escaleno";

return "Triangulo desconhecido";

}

private bool confereTriangulo(int lado1, int lado2, int lado3)
{
if ((lado1 + lado2) < (lado3) || (lado1 + lado3) < (lado2) || (lado3 + lado2) < (lado1)) return true;
return false;
}
}
}

Nosso primeiro exemplo de teste de unidade era um teste totalmente feito no código fonte da classe “HardCodedUnitTests.cs” abaixo:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using UniBhTirangle;

namespace TestProject1
{
[TestClass]
public class HardCodedUnitTests
{
///
/// Teste para validar triangulo equilátero
///
[TestMethod]
public void TrianguloEquilatero()
{
//Configurando teste
Triangulo triangulo = new Triangulo();

&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;nbsp;

// Dados que serão usados
int lado1 = 5;
int lado2 = 5;
int lado3 = 5;

// Resultado esperado
string resultadoEsperado = "Equilátero";

// Procedimentos
string resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}

///
/// Teste para validar triangulo Isósceles
///
[TestMethod]
public void TrianguloIsosceles()
{
//Configurando teste
Triangulo triangulo = new Triangulo();

// Dados que serão usados

int lado1 = 5;
int lado2 = 5;
int lado3 = 6;

// Resultado esperado
string resultadoEsperado = "Isósceles";

// Procedimentos
string resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}

///
/// Teste pra validar triangulo escaleno
///
[TestMethod]
public void TrianguloEscaleno()
{
//Configurando teste
Triangulo triangulo = new Triangulo();

// Dados que serão usados

int lado1 = 5;
int lado2 = 8;
int lado3 = 9;

// Resultado esperado
string resultadoEsperado = "Escaleno";

// Procedimentos
string resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}

///
/// Teste para validar se quando a soma de dois lados é maior que o outro lado considera triangulo inválido
///

[TestMethod]

public void LadoMaiorQueSomaDosOutrosDois()
{
//Configurando teste
Triangulo triangulo = new Triangulo();

// Dados que serão usados
int lado1 = 5;
int lado2 = 3;
int lado3 = 15;

// Resultado esperado
string resultadoEsperado = "Inválido1";

// Procedimentos
string resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}
}
}

Como pudemos ver acima, cada método de teste tem o objetivo de testar uma funcionalidade do sistema, mas ainda assim está limitado a testar apenas um aspecto. Caso queira testar mais de um aspecto, este modelo você deve criar o número de aspectos de cada funcionalidade em métodos de teste de unidade, o que pode te deixar com uma classe com mais de 50 ou 60 métodos de teste. Para deixar a situação um pouco mais complicada, vimos que repetimos boa parte do código fonte em todos os métodos, o que nos faz pensar que se uma modificação de requisitos acontecer, por menor que seja, vai nos obrigar a refatorar dezenas de métodos em cada classe.

A execução demostrada acima não consegui capturar nenhum defeito.

No segundo exemplo, retiramos algumas atribuições como pode ser visto na classe “HardCodedUnitTestsWithBasicReuse.cs“, mas vimos que mesmo assim não obtivemos real reuso de código fonte.

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using UniBhTirangle;

namespace TestProject1
{
[TestClass]
public class HardCodedUnitTestsWithBasicReuse
{
Triangulo triangulo = new Triangulo();
int lado1;
int lado2;
int lado3;
string resultadoEsperado = string.Empty;
string resultadoAtual = string.Empty;

///
/// Teste para validar triangulo equilátero
///
[TestMethod]
public void TrianguloEquilatero()
{

// Dados que serão usados

lado1 = 5;
lado2 = 5;
lado3 = 5;

// Resultado esperado
resultadoEsperado = "Equilátero";

// Procedimentos
resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}

///
/// Teste para validar triangulo Isósceles
///
[TestMethod]
public void TrianguloIsosceles()
{
//Configurando teste
Triangulo triangulo = new Triangulo();

&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;nbsp;

// Dados que serão usados
lado1 = 5;
lado2 = 5;
lado3 = 6;

// Resultado esperado
resultadoEsperado = "Isósceles";

// Procedimentos
resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}

///
/// Teste pra validar triangulo escaleno
///
[TestMethod]
public void TrianguloEscaleno()
{
//Configurando teste
Triangulo triangulo = new Triangulo();

&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;nbsp;

// Dados que serão usados
lado1 = 5;
lado2 = 8;
lado3 = 9;

// Resultado esperado
resultadoEsperado = "Escaleno";

// Procedimentos
resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}

///
/// Teste para validar se quando a soma de dois lados é maior que o outro lado considera triangulo inválido
///

[TestMethod]
public void LadoMaiorQueSomaDosOutrosDois()
{
//Configurando teste
Triangulo triangulo = new Triangulo();

// Dados que serão usados
lado1 = 5;
lado2 = 3;
lado3 = 15;

// Resultado esperado
resultadoEsperado = "Inválido1";

// Procedimentos
resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}
}
}

Apesar do reuso de declarações, não pudemos notar nada de melhoria, mas agora nossos testes economizam espaço em memória. Pode parecer pouco, mas em projetos com restrições de memória como projetos para firmware de no-breaks em que temos entre 14kb e 64kb de RAM, isso faz uma diferença gigantesca.

A execução acima aparentemente é a mesma e também não identificou nenhum defeito.

No nosso terceiro exemplo na classe “HardCodedUnitTestsWithBasicReuse2.cs“, apesar de não ficar muito claro, já implementamos uma “técnica” que ajuda a promover algum reuso em quantidades significativas, a separação de itens que realizam a iniciação de todos os testes em um único método.

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using UniBhTirangle;

namespace TestProject1
{
[TestClass]
public class HardCodedUnitTestsWithBasicReuse2
{
Triangulo triangulo = new Triangulo();
int lado1;
int lado2;
int lado3;
string resultadoEsperado = string.Empty;
string resultadoAtual = string.Empty;

[TestInitialize()]
public void preparaTestes()
{
lado1 = 5;
}

///
/// Teste para validar triangulo equilátero
///

[TestMethod]
public void TrianguloEquilatero()
{

// Dados que serão usados
lado2 = 5;
lado3 = 5;

// Resultado esperado
resultadoEsperado = "Equilátero";

// Procedimentos
resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

Assert.AreEqual(resultadoEsperado, resultadoAtual);

}

///
/// Teste para validar triangulo Isósceles
///

[TestMethod]
public void TrianguloIsosceles()
{
//Configurando teste
Triangulo triangulo = new Triangulo();

// Dados que serão usados
lado2 = 5;
lado3 = 6;

// Resultado esperado
resultadoEsperado = "Isósceles";

// Procedimentos
resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}

///
/// Teste pra validar triangulo escaleno
///

[TestMethod]
public void TrianguloEscaleno()
{
//Configurando teste
Triangulo triangulo = new Triangulo();

// Dados que serão usados
lado2 = 8;
lado3 = 9;

// Resultado esperado
resultadoEsperado = "Escaleno";

// Procedimentos
resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}

///
/// Teste para validar se quando a soma de dois lados é maior que o outro lado considera triangulo inválido
///

[TestMethod]
public void LadoMaiorQueSomaDosOutrosDois()
{
//Configurando teste
Triangulo triangulo = new Triangulo();

// Dados que serão usados
lado2 = 3;
lado3 = 15;

// Resultado esperado
resultadoEsperado = "Inválido1";

// Procedimentos
resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}
}
}

Quando usamos um método com o attribute [TestInitialize()] dizemos que todos os testes da classe devem executar esse método antes de iniciar os testes, ou seja, em casos de teste com as mesmas pré-condições deixamos de escrever as pré-condições para eles e passamos a reusar essas inicializações e atribuições e em casos de mudanças temos menos modificações para realizar.

Ainda não encontramos nenhum defeito e aparentemente o resultado é o mesmo.

No nosso quarto exemplo na classe “DataDrivenUnitTestsWithGoodReuse.cs“, deixamos de usar o Microsoft Unit Test Framework para usar o NUnit que tem um recurso muito parecido com o que usamos no JUnit no post “Uma Indrodução a TDD com JUnit“, mas aqui ainda mais simples e mais dirigido a dados ainda:

using UniBhTirangle;
using NUnit.Framework;

namespace TestProject1
{
[TestFixture]
public class DataDrivenUnitTestsWithGoodReuse
{
Triangulo triangulo = new Triangulo();
string resultadoAtual = string.Empty;

///
/// Teste para validar triangulo equilátero
///

[TestCase(5, 5, 5, "Equilátero")]
[TestCase(6, 6, 6, "Equilátero")]
[TestCase(7, 7, 7, "Equilátero")]
public void TrianguloEquilatero(int lado1, int lado2, int lado3, string resultadoEsperado)
{

// Procedimentos
resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}

///
/// Teste para validar triangulo Isósceles
///

[TestCase(5, 5, 6, "Isósceles")]
[TestCase(5, 6, 5, "Isósceles")]
[TestCase(6, 5, 5, "Isósceles")]
public void TrianguloIsosceles(int lado1, int lado2, int lado3, string resultadoEsperado)
{

// Procedimentos
resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}

///
/// Teste pra validar triangulo escaleno
///

[TestCase(5, 6, 7, "Escaleno")]
[TestCase(5, 7, 6, "Escaleno")]
[TestCase(7, 6, 5, "Escaleno")]
public void TrianguloEscaleno(int lado1, int lado2, int lado3, string resultadoEsperado)
{
// Procedimentos
resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}

///
/// Teste para validar se quando a soma de dois lados é maior que o outro lado considera triangulo inválido
///

[TestCase(5, 6, 17, "Inválido1")]
[TestCase(5, 15, 5, "Inválido1")]
[TestCase(9, 5, 25, "Inválido1")]
public void LadoMaiorQueSomaDosOutrosDois(int lado1, int lado2, int lado3, string resultadoEsperado)
{
// Procedimentos
resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação
Assert.AreEqual(resultadoEsperado, resultadoAtual);
}
}
}

Como pudemos ver, agora temos um método e usamos a Notation "TestCase" para informar um conjunto de valores que queremos passar para o método de teste baseado na sua assinatura, ou seja, criamos um método parametrizado para o nosso teste. Assim reduzimos consideravelmente o número de linhas de código e caso tenhamos uma mudança de requisitos retemos menos linhas para refatorar e maior agilidade para escrever os testes.

Para usar esse modelo, precisei instalar o NUnit <a href="http://www.nunit.org/?p=download">http://www.nunit.org/?p=download</a> e vinculá-lo nas referências do projeto, além de usar um programa externo à minha ferramenta IDE (Visual Studio).

Como pudemos ver no exemplo acima, com muito menos linhas de código executamos muito mais testes e testes mais fáceis de configurar, que já foram capazes de encontrar defeitos. Como último exemplo temos o teste baseado em uma tabela do Excel como demonstrado no post “Testes de unidade para quem não sabe nada de programação“, como pode ser visto na classe “DataDrivenUnitTestsWithGoodReuse2“:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using UniBhTirangle;

namespace TestProject1
{
[TestClass]
public class DataDrivenUnitTestsWithGoodReuse2
{
Triangulo triangulo = new Triangulo();
int lado1;
int lado2;
int lado3;
string resultadoEsperado = string.Empty;
string resultadoAtual = string.Empty;

// Adaptação realizada para utilizar o Data Source no TestContext
private TestContext testContextInstancia;

public TestContext TestContext
{
get
{
return testContextInstancia;
}
set
{
testContextInstancia = value;
}
}

///
/// Teste para validar triangulo equilátero
///

[DataSource("System.Data.Odbc",
"Dsn=Excel Files;dbq=|DataDirectory|\\testeDadosTriangulo.xlsx;defaultdir=C:\\;driverid=1046;maxbuffersize=2048;pagetimeout=5",
"Plan1$",
DataAccessMethod.Sequential),
DeploymentItem("TestProject1\\testeDadosTriangulo.xlsx"),
TestMethod]
public void TrianguloTest()
{

// Atribuições realizadas através do datasource configurado
int lado1 = int.Parse(TestContext.DataRow["lado1"].ToString());
int lado2 = int.Parse(TestContext.DataRow["lado2"].ToString());
int lado3 = int.Parse(TestContext.DataRow["lado3"].ToString());

// Procedimentos de teste
resultadoEsperado = TestContext.DataRow["resultado"].ToString();
resultadoAtual = triangulo.retornaTipo(lado1, lado2, lado3);

// Comparação ou  resultado esperado
Assert.AreEqual(resultadoEsperado, resultadoAtual);

}
}
}

E temos aqui um resultado que mostra que ao reduzir ainda mais o número de linhas de código, e abstrair ainda mais os dados de teste conseguimos realizar mais testes e com mais efetividade ainda:

Espero que tanto os conceitos quanto as explicações técnicas realizadas durante a palestra estejam fáceis de entender aqui. Fiquem a vontade para comentar, criticar e sugerir melhorias neste e em outros posts :)

A internet está cheia de exemplos, vídeos, demonstrações e posts sobre testes de unidade e TDD, basta pesquisar. Ao final deste post temos uma lista de boas referências. Recomento alguns livros sobre o assunto, pois sempre tem conteúdo com mais confiabilidade.

A palestra do Charles Fortes pode ser baixada aqui: http://api.ning.com/files/0VwKCJU1B-6JQgwlriZWyyiYAO8f4fWGAYNxkL35nrt2dQVlZ7FQOzAmSpyTYe8U8wunKGfks7F*krvA8xeQlPl4QUtJy*-0/TDD.pdf

Meu muito obrigado aos alunos da UNIBH que tiveram muita paciência e curiosidade para ficar até o final da palestra (mesmo estourando um pouco do tempo) e que participaram e questionaram alguns aspectos..

Referências e estudos:

•Modelo V descrevendo o paralelismo entre as atividades de desenvolvimento e teste de software (CRAIG e JASKIEL, 2002)
http://artofunittesting.com/ (em 16/05/2011)
http://www.bugbang.com.br/?p=1661 (Vídeo) (em 16/05/2011)
http://www.bugbang.com.br/?p=839 (Exemplo prático) (em 16/05/2011)
http://artofunittesting.com/ (Vídeo) (em 16/05/2011)

Bons testes :)

Camilo Ribeiro

Test Engineer at Klarna
Desenvolvedor, testador e agilista desde 2005, atualmente trabalhando na Suécia.

3 thoughts on “Palestra UniBH: Desenvolvimento Dirigido por Testes

  1. Grande Camilo… como sempre com ótimos trabalhos =D

    Uma dúvida/sugestão para seu trabalho…

    Suas palestras são gravadas? Seria ótimo poder assisti-las.

    Muito bom também o uso das Referências…

    =)

  2. Concordo que essas palestras deveriam ser gravadas. (;
    No blog do James Whittaker ele comenta que no google existem funções do programador e do testador, para emlhorar a qualidade e a cobertura dos testes. Vale a pena a leitura!

  3. Doutores Percy e VB,

    Muito obrigado pelos vários comentários :) (e desculpe a demora pra responder)

    Infelizmente não é tão fácil gravar uma palestra, mas vou tentar fazer um “live” (web conferencia) qualquer dia desses, tipo uma “mesa redonda” (se me permite plagiar a criação do Fabrício) on-line com participação da turma de teste de software, incluindo vocês.

    Mas o texto ajuda muito o pessoal a achar mais rápido o que gosta e evita que as pessoas vejam minha gagueira esporádica. rsrs

    Abraços

Leave a Reply