Orientação a Objeto - Testes de Unidade
Objetivos da seção
- Discutir a importância fundamental de testes no
desenvolvimento de software
- Apresentar Testes de Unidade como forma básica de testar
software
- Aprender a escolher bons testes
Um exemplo: Tratamento de valores monetários no pacote p1.aplic.banco
- Nesta sub-seção, só queremos construir exemplos que servirão adiante para
conversarmos sobre testes de software
Por que não usar double para valores financeiros?
- Muitos programadores usam o tipo básico double para representar valores financeiros
- Isso não é uma boa idéia por dois motivos
- Primeiro motivo: há perda de precisão
- Quer ver? Rode o programa Grana1, abaixo
public class Grana1 {
public static void main(String[] args) {
double x, y;
x = 10000000000.0;
y = 0.01;
for(int i = 0; i < 10; i++) {
System.out.println(x+y);
x *= 10;
}
}
}
- Observe que estamos somando 1 centavo (0.01) a um valor double
- A saída é como segue:
1.000000000001E10
1.0000000000001E11
1.00000000000001E12
1.000000000000001E13
1.0000000000000002E14
1.0E15
1.0E16
1.0E17
1.0E18
1.0E19
- O tipo básico double tem aproximadamente 15 dígitos de precisão
- Com float, a coisa é pior: aproximadamente 7 dígitos de precisão
- O segundo problema é que, em aplicações financeiras, não queremos lidar com
frações de centavos
- Examine a saída do programa que segue (Grana2)
public class Grana2 {
public static void main(String[] args) {
double x = 1.00;
for(int i = 0; i < 10; i++) {
System.out.println(x);
// aplica 10% de juros
x += x * 0.1;
}
}
}
1.0
1.1
1.2100000000000002
1.3310000000000002
1.4641000000000002
1.61051
1.7715610000000002
1.9487171
2.1435888100000002
2.357947691
- Imagine se seu extrato bancário mostrasse valores assim!
- Claro que podemos lidar com isso no programa mas é chato
Uma classe para representar valores financeiros
- Para resolver os problemas acima, poderíamos usar BigDecimal
- Porém, prefiro construir nossa solução do zero usando o conceito de "ponto
fixo" em vez de "ponto flutuante"
- A idéia é simples: se todos os valores financeiros fossem expressos em centavos, não
haveria necessidade de manter frações
- Na hora de imprimir valores, podemos dividir por cem e imprimir os reais e pegar o resto
da divisão com 100 e imprimir os centavos
- Portanto o "ponto decimal" está fixo em duas casas
- Exemplo:
- R$34,56 seria representado como o valor 3456
- Na hora de imprimir o valor, imprimimos:
- 3456 / 100 = 34 (reais)
- 3456 % 100 = 56 (centavos)
- Agora, perguntamos: qual tipo básico usar para representar valores financeiros?
- Se usarmos "int" de 32 bits, poderemos representar a faixa -231 a 231-1
ou -R$21.474.836,48 a +R$21.474.836,47
- Se usarmos "long" de 64 bits, poderemos representar a faixa -263 a
263-1 ou -R$92.233.720.368.547.758,08 a +R$92.233.720.368.547.758,07
- Tem que usar long, certo?
- Se precisar de valores mais altos, use BigDecimal que não tem limite de precisão
- Vamos encapsular toda essa mágica numa classe chamada Real2
- Ela não se chama Real porque Real é outra classe que será mostrada adiante
- Real2 representa a moeda "Real" mas tem bugs. Adiante, veremos como remover o
bug
- Vamos primeiro ver a documentação
de Real2
- Observe que, pasa facilitar a vida do usuário, podemos tratar valores financeiros com
double ainda
Os testes de unidade
- Vamos fazer testes de unidade para testar a classe e achar
erros
- Testes de unidade são programas que testam classes individuais
- A "unidade" sendo testada é a classe
- Regra geral: Um código não testado não funciona
- Então vamos testar ... mas como?
- É muito importante que o esforço que vamos colocar nos testes seja reutilizado mais na
frente, caso tenhamos que alterar o código ao longo da vida do software
- Portanto, queremos automatizar os testes
- Queremos clicar um botão a qualquer momento para testar tudo!
- Pelo menos um teste por dia (frequentemente vários testes por hora)
- Uso de um framework de testes para ajudar a testar
- Absolutamente necessário ter testes de unidade, principalmente se fizer refactoring de
código
- Muitos programadores escrevem os testes antes de escrever o código!
- Testar as classes da menos acoplada para a mais acoplada
- Na realidade, segue a ordem de desenvolvimento
- Desenvolvimento e testes de unidade feitos em paralelo!
Uso do framework de testes JUNIT
Testes de unidade da classe Real2
- Vejamos agora TestaReal2.java que contém testes de unidade para a classe Real2 (a ser
escrita)
package p1.aplic.bancotestes;
import junit.framework.*;
import p1.aplic.banco.*;
/**
* Testes da classe Real2.
*
*/
public class TestaReal2 extends TestCase {
protected Real2 zero;
protected Real2 zero2;
protected Real2 quaseZero;
protected Real2 umCentavo;
protected Real2 menosUmCentavo;
protected Real2 menosUmEVinteETres;
public TestaReal2(String name) {
super(name);
}
protected void setUp() {
zero = new Real2();
zero2 = new Real2(0.0);
quaseZero = new Real2(0.004999);
umCentavo = new Real2(0.01);
menosUmCentavo = new Real2(-0.01);
menosUmEVinteETres = new Real2(-1.23);
}
public void testEquals() {
assertTrue(!zero.equals(null));
assertEquals(zero, zero);
assertEquals(zero, zero2);
assertEquals(new Real2(0.01), umCentavo);
assertTrue(!zero.equals(umCentavo));
assertEquals(zero, quaseZero);
}
public void testCompareTo() {
assertEquals("1", 0, zero.compareTo(zero));
assertEquals("2", 0, zero.compareTo(new Real2()));
assertEquals("3", 0, zero.compareTo(zero2));
assertTrue("4", zero.compareTo(umCentavo) < 0);
assertTrue("5", zero.compareTo(menosUmCentavo) > 0);
assertEquals("6", 0, zero.compareTo(quaseZero));
}
public void testGetValor() {
assertEquals("1", 0.0, zero.getValor(), 0.0);
assertEquals("2", 0.0, zero2.getValor(), 0.0);
assertEquals("3", 0.0, quaseZero.getValor(), 0.0);
assertEquals("4", 0.01, umCentavo.getValor(), 0.0);
assertEquals("5", -0.01, menosUmCentavo.getValor(), 0.0);
}
public void testToString() {
assertEquals("1", "R$0,00", zero.toString());
assertEquals("2", "R$0,00", quaseZero.toString());
assertEquals("3", "R$0,01", umCentavo.toString());
assertEquals("4", "R$-1,23", menosUmEVinteETres.toString());
assertEquals("5", "R$-0,01", menosUmCentavo.toString());
}
public void testSetValor() {
Real2 z1 = new Real2();
z1.setValor(0.004999);
assertEquals("1", 0.0, z1.getValor(), 0.0);
z1.setValor(0.01);
assertEquals("2", 0.01, z1.getValor(), 0.0);
z1.setValor(-0.01);
assertEquals("3", -0.01, z1.getValor(), 0.0);
}
}
- No programa acima:
- Os testes são os métodos que iniciam com "test"
- Antes de cada método de teste, setup() é chamada pelo framework
- Os métodos "assert" são usados para testar se tudo está ok
- Se houver problemas, junit vai avisar
- "Assert" significa "Afirmar, declarar"
- Implemente o suficiente da classe Real2 para os testes pelo menos compilarem
- Agora, vamos executar os testes (via Eclipse, por exemplo)
- Claro que nenhum teste passa, pois nem escrevemos a lógica da classe!
- Aqui, o professor pode implementar a classe em aula fazendo os testes passarem um por um
- Para facilitar a apresentação deste material, eis uma tentativa de implementação:
/*
* Desenvolvido para a disciplina Programacao 1
* Curso de Bacharelado em Ciência da Computação
* Departamento de Sistemas e Computação
* Universidade Federal da Paraíba
*
* Copyright (C) 1999 Universidade Federal da Paraíba.
* Não redistribuir sem permissão.
*/
package p1.aplic.banco;
import java.io.*;
/**
* A moeda Real (Brasil). Tem corte automático de frações de centavos.
* O motivo da existência dessa classe é de permitir a manipulação
* de valores financeiros sem se preocupar com frações de centavos.
* Classes clientes podem usar double para manipular valores mas,
* ao passar tais valores para uma Moeda, as frações de centavos
* somem.
*
* ****** Esta classe tem erros e serve para ensinar testes de unidade ******
*
* @author Jacques Philippe Sauvé, jacques@dsc.ufpb.br
* @version 1.1
* <br>
* Copyright (C) 1999 Universidade Federal da Paraíba.
*/
public class Real2 {
private long centavos;
/**
* Controi um valor 0.0 em moeda Real
*/
public Real2() {
this(0.0);
}
/**
* Controi um valor em moeda Real.
* @param valor O valor em reais.
*/
public Real2(double valor) {
setValor(valor);
}
/**
* Recupera o valor como double.
*/
public double getValor() {
return centavos / 100.0;
}
/**
* Ajusta o valor a ser representado como moeda.
* @param valor O valor a representar como moeda.
*/
public void setValor(double valor) {
// Vai perder frações de centavos aqui
// Ajustar 0.5 centavos é para arredondar ao
// centavo mais próximo
// Tem um erro aqui (vide Moeda.java para ver o correto)
setCentavos((long)(valor*100.0 + 0.5));
}
/**
* Compara igualdade de duas moedas.
* @param moeda O outro valor a comparar.
*/
public boolean equals(Object moeda) {
if(!(moeda instanceof Real2)) {
return false;
}
return getValor() == ((Real2)moeda).getValor();
}
/**
* Compara dois valores de moeda.
* @param outra A outra moeda a comparar.
* @return 0 se a moeda for igual à outra moeda; -1 se ela for menor e +1 se for maior.
*/
public int compareTo(Real2 outra) {
long diferença = getCentavos() - outra.getCentavos();
return diferença == 0 ? 0 : (diferença < 0 ? -1 : 1);
}
/**
* Representa o valor da moeda como string
*/
public String toString() {
String resultado = "R$";
long centavos = getCentavos();
long cent = centavos % 100;
resultado += centavos/100 + "," + (cent < 10 ? "0" : "") + cent;
return resultado;
}
protected long getCentavos() {
return centavos;
}
protected void setCentavos(long centavos) {
this.centavos = centavos;
}
}
- O código parece legal, não?
- setValor() tem que cuidar de cortar frações de centavos que não interessam
- toString() deve retornar o valor como String
- A saída dos testes é como segue:

- Eca! Estamos com um monte de problemas!
- De 5 testes, 4 deram errado!
- Vamos ver o primeiro erro: testCompareTo número 5 deu pau
- Esse teste é o seguinte:
assertTrue("5", zero.compareTo(menosUmCentavo) > 0);
- Aparentemente, menosUmCentavo está com problema
- O segundo erro é mais revelador:
Failure: testGetValor(p1.aplic.bancotestes.TestaReal2):5 expected:<-0.01> but was:<0.0>
- Opa! Parece que o valor de menosUmCentavo acabou sendo 0.0
- Como isso aconteceu?
- Seguindo o que ocorre com o valor menosUmCentavo rapidamente revela o problema
- setValor não corta as frações de centavos adequadamente para números negativos
- Eis uma versão consertada
public void setValor(double valor) {
// Vai perder frações de centavos aqui
// Ajustar 0.5 centavos é para arredondar ao
// centavo mais próximo
if(valor >= 0) {
this.centavos = (long)(valor*100.0 + 0.5);
} else {
this.centavos = (long)(valor*100.0 - 0.5);
}
}
- Rodamos o programa novamente e vemos que consertamos o problema mas continuamos com um
problema no teste:
Failure: testGetValor(p1.aplic.bancotestes.TestaReal2):4 expected:<R$-1,23> but was:<R$-1,0-22>
- Aparentemente, números negativos estão causando problemas no método toString()
- Examinando a situação, descobrimos logo o erro
- A versão consertada segue abaixo
public String toString() {
String resultado = "R$";
long centavos = getCentavos();
if(centavos < 0) {
resultado += "-";
centavos = -centavos;
}
long cent = centavos % 100;
resultado += centavos/100 + "," + (cent < 10 ? "0" : "") + cent;
return resultado;
}
- Agora, rodamos os testes novamente e tudo dá certo
- O mais importante é que esses testes podem ser rodados a qualquer momento no futuro,
principalmente, quando o código é alterado
- Examine todos os testes de unidade do pacote p1 para ver como foram feitos
- Para rodar todos os testes, execute packagep1\testa.bat
Uma solução mais genérica
- O que segue não tem a ver com testes de unidade mas procura melhorar a solução de
tratamento de moedas
- A solução acima só trata da moeda Real
- Uma melhor solução, mais genérica, pode ser vista no pacote p1
- A classe Moeda trata de todos os aspectos gerais de uma moeda que possui centavos
- Ela é abstrata porque não existe tal moeda genérica
- Por exemplo, é impossível escrever toString()
/*
* Desenvolvido para a disciplina Programacao 1
* Curso de Bacharelado em Ciência da Computação
* Departamento de Sistemas e Computação
* Universidade Federal da Paraíba
*
* Copyright (C) 1999 Universidade Federal da Paraíba.
* Não redistribuir sem permissão.
*/
package p1.aplic.banco;
import java.io.*;
/**
* Classe abstrata representando uma moeda genérica com centavos.
* O motivo da existência dessa classe é de permitir a manipulação
* de valores financeiros sem se preocupar com frações de centavos.
* Classes clientes podem usar double para manipular valores mas,
* ao passar tais valores para uma Moeda, as frações de centavos
* somem.
* <P>A classe é abstrata porque não sabemos o nome da moeda e não temos portanto
* um método toString().
*
* @author Jacques Philippe Sauvé, jacques@dsc.ufpb.br
* @version 1.1 (adicionei setCentavos)
* <br>
* Copyright (C) 1999, 2000 Universidade Federal da Paraíba.
*/
public abstract class Moeda implements Serializable {
private long centavos;
/**
* Controi um valor 0,0 como Moeda.
*/
public Moeda() {
this(0.0);
}
/**
* Controi um valor como Moeda.
* @param valor O valor a representar.
*/
public Moeda(double valor) {
setValor(valor);
}
/**
* Recupera o valor como double.
*/
public double getValor() {
return centavos / 100.0;
}
/**
* Ajusta o valor a ser representado como moeda.
* @param valor O valor a representar como moeda.
*/
public void setValor(double valor) {
// Vai perder frações de centavos aqui
// Ajustar 0.5 centavos é para arredondar ao
// centavo mais próximo
if(valor >= 0) {
this.centavos = (long)(valor*100.0 + 0.5);
} else {
this.centavos = (long)(valor*100.0 - 0.5);
}
}
/**
* Compara igualdade de duas moedas.
* @param moeda O outro valor a comparar.
*/
public boolean equals(Object outroObjeto) {
if(!(outroObjeto instanceof Moeda)) {
return false;
}
return getValor() == ((Moeda)outroObjeto).getValor();
}
/**
* Compara dois valores de moeda.
* @param outra A outra moeda a comparar.
* @return 0 se a moeda for igual à outra moeda; -1 se ela for menor e +1 se for maior.
*/
public int compareTo(Moeda outra) {
long diferença = getCentavos() - outra.getCentavos();
return diferença == 0 ? 0 : (diferença < 0 ? -1 : 1);
}
/**
* Representa o valor da moeda como string.
*/
public abstract String toString();
protected long getCentavos() {
return centavos;
}
protected void setCentavos(long centavos) {
this.centavos = centavos;
}
}
- Agora, podemos criar a moeda específica Real:
/*
* Desenvolvido para a disciplina Programacao 1
* Curso de Bacharelado em Ciência da Computação
* Departamento de Sistemas e Computação
* Universidade Federal da Paraíba
*
* Copyright (C) 1999 Universidade Federal da Paraíba.
* Não redistribuir sem permissão.
*/
package p1.aplic.banco;
import java.io.*;
/**
* A moeda Real (Brasil). O importante aqui é o método toString que
* sabe o símbolo da moeda (R$).
*
* @author Jacques Philippe Sauvé, jacques@dsc.ufpb.br
* @version 1.1
* <br>
* Copyright (C) 1999 Universidade Federal da Paraíba.
*/
public class Real extends Moeda implements Serializable {
/**
* Controi um valor 0.0 em moeda Real
*/
public Real() {
this(0.0);
}
/**
* Controi um valor em moeda Real.
* @param valor O valor em reais.
*/
public Real(double valor) {
super(valor);
}
/**
* Representa o valor da moeda como string
*/
public String toString() {
String resultado = "R$";
long centavos = getCentavos();
if(centavos < 0) {
resultado += "-";
centavos = -centavos;
}
long cent = centavos % 100;
resultado += centavos/100 + "," + (cent < 10 ? "0" : "") + cent;
return resultado;
}
}
- Como exercício de casa, faça com que Moeda não seja abstrata e tenha um toString()
"razoável"
- Agora, deve ser possível simplificar a classe Real
O que Testar?
- Teste todo o comportamento
- Procure testar de forma a forçar a execução de todos os comandos
- Exemplo: forçar o código a entrar no "if" e forçar a entrar no
"else"
- Escolha valores de dados que representam condições limites
- Para descobrir erros "off-by-one"
- Quando se usa "<" em vez de "<=", etc.
- Isso é uma fonte muito, muito comum de bugs
- Se há uma faixa de valores para a qual o programa deve funcionar, teste os seguintes
valores
- Primeiro valor da faixa
- Primeiro valor da faixa -1
- Valor médio
- Último valor da faixa
- Último valor da faixa + 1
- Sempre teste a entrada vazia
- Sempre teste valores negativos e o valor zero
- Quando parar de testar?
- Faça tantos testes quantos necessários para se convencer que a implementação está
ok
- Inclua testes que "têm possibilidade de dar retorno" (descobrir problemas)
- Não inclua testes "besta"
- Porém, cuidado para não achar que "tudo é teste besta"
- A quantidade de testes e formas de testar depende da robustez desejada para o software
- Fazemos "Good enough software", não software (impossivelmente) perfeito
- Testes provam a presença de erros, mas nunca sua ausência (Dijkstra)
Tipos de Testes
- Há vários tipos de testes de software
- Testes de unidade
- Para testar classes individuais
- Feitos pelo próprio programador da classe
- Testa toda a interface da classe
- Testes funcionais (ou testes de aceitação)
- Para testar os Use Cases
- Especificados pelo cliente, se possível, e codificados pelo time de desenvolvimento
- Testes de sistema
- Uso de Total System Environment
- Incluindo outros produtos de software, todas as plataformas, todas as configurações,
etc.
- Frequentemente feitos por uma equipe independente de testes
- Testes de regressão
- Antes de por o sistema na rua, mesmo que tenha havido apenas uma recompilação
- Normalmente um subconjunto dos testes de sistema
- Frequentemente feitos por uma equipe independente de testes
- Teste alfa
- Teste de produto (com embalagem manual, etc.) num "cliente" dentro da empresa
- Feitos pelo pessoal de marketing
- Teste beta
- Como teste alfa mas incluindo clientes fora da empresa
- Feitos pelo pessoal de marketing
Algumas palavras de Beck e Gamma
- Sometimes you just won't feel like writing tests, especially at first. Don't. However,
pay attention to how much more trouble you get into, how much more time you spend
debugging, and how much more stress you feel when you don't have tests. We have been
amazed at how much more fun programming is and how much more aggressive we are willing to
be and how much less stress we feel when we are supported by tests. The difference is
dramatic enough to keep us writing tests even when we don't feel like it.
oo-7 programa anterior
próxima