Material baseado nos slides do prof. Júlio Machado
O nivel de teste unitário é todo baseado na construção de classes driver
Para cada classe que se deseja testar deve-se construir uma classe driver de teste que exercita a classe original procurando identificar defeitos
A execução do teste unitário implica na execução dos métodos de teste que compõem as classes drivers
public class ContaCorrenteTest {
public void testaDeposito() {
ContaCorrente target = new ContaCorrente(100,"Fulano");
target.depositar(1000.0);
if (target.getSaldo() == 1000.0)
System.out.println("TestaDeposito: Pass");
else System.out.println("TestaDeposito: Fail");
}
public void testaRetirada() {
ContaCorrente target = new ContaCorrente(100,"Fulano");
target.depositar(6000.0);
target.retirar(1000.0);
if (target.getSaldo() == 5000.0)
System.out.println("TestaRetirada: Pass");
else System.out.println("TestaRetirada: Fail");
}
public void testaDepositoNegativo {
...
}
}
Exige que se reflita sobre as funcionalidades da classe e sua implementação antes de seu desenvolvimento
Permite a identificação rápida dos defeitos mais simples
Permite garantir que a classe cumpra um conjunto de requisitos mínimos (aqueles garantidos pelos testes)
Facilita a detecção de defeitos no caso de manutenção ou refactoring
Necessidade de construção do "cenário" em cada método
Necessidade de construir um programa para execução dos casos de teste
Dificuldade em se trabalhar com grandes conjuntos de dados de teste
Dificuldade para coletar os resultados
Dificuldade para automatizar a execução dos testes
Foi criado no contexto do surgimento do eXtreme Programming em 1998
Permite a criação de testes unitários:
Sua concepção adapta-se facilmente aos IDEs de desenvolvimento
JUnit: versão para Java do framework
import ContaCorrente.*;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
public class ContaCorrenteTest {
@Test // Anotações
public void retirarTest() {
ContaCorrente target = new ContaCorrente(100, "Fulano");
target.depositar(5000.0);
target.retirar(1000);
assertEquals(4000.0, target.getSaldo(), 0.1); // Asserções
}
@Test
public void depositarTest() {
ContaCorrente target = new ContaCorrente(100, "Fulano");
target.depositar(5000.0);
target.depositar(1000.0);
assertEquals(6000.0, target.getSaldo(), 0.1);
}
}
São rótulos incluídos no código fonte e que servem para indicar o papel do método na classe driver
Asserções são declarações do que acreditamos ser correto
Quando construímos classes drivers, usamos asserções para verificar as condições que desejamos testar
O JUnit captura as exceções lançadas pela asserções
Quando uma asserção falha ela lança uma exceção, que é capturada pelo JUnit, que detecta a falha
As falhas detectadas pelo JUnit são então compiladas em um relatório
O salário líquido é calculado descontando-se 10% de INSS e mais 12% de IR sobre o que exceder R$ 2000,00
public class Funcionario {
private String nome;
private int codigo;
private double salarioBase;
...
void setSalarioBase(double salB) { this.salarioBase = salB; }
public double getSalarioLiquido() {
double inss = salarioBase * 0.1;
double ir = 0.0;
if (salarioBase > 2000.0) {
ir = (salarioBase-2000.0)*0.12;
}
return salarioBase - inss - ir;
}
}
== 0.0
)public class FuncionarioTest {
private Funcionario func;
@Before
public void setUp() {
func = new Funcionario(200,"Ze",1900.0);
}
@Test
public void testGetSalLiquidoMenosQue2000() {
double expected = 1710;
double actual = func.getSalarioLiquido();
assertEquals(expected, actual, 0.1);
}
@Test
public void testGetSalLiquidoIgual2000() {
func.setSalarioBase(2000.0);
double expected = 1900;
double actual = func.getSalarioLiquido();
assertEquals(expected, actual, 0.1);
}
@Test
public void testGetSalLiquidoMaior2000() {
func.setSalarioBase(3000.0);
double expected = 2580;
double actual = func.getSalarioLiquido();
assertEquals(expected, actual, 0.1);
}
}
@BeforeClass
: executa no início da bateria de testes (fixture)@AfterClass
: executa quando encerra a bateria de testes@Before
: executa antes de cada teste@After
: executa depois de cada teste@Test
: indica um método de testeassertEquals
: deprecated para tipos ponto de flutuanteassertFalse
: sobre uma condiçãoassertTrue
: sobre uma condiçãoassertNull
: sobre uma referênciaassertNotNull
: sobre uma referênciaassertSame
: se referenciam o mesmo objeto (não equals)assertNotSame
: oposto de assertSame
fail(msg)
: falha o teste e exibe msgpublic class Parquimetro {
// Permite inserir moedas no parquimetro (soma no saldo)
// Valores possíveis: 1, 5, 10, 25, 50 e 100 (1 real)
// Gera InvalidValueException no caso de valor inválido
public void insereMoeda(int valor) throws InvalidValueException {}
// Retorna o saldo acumulado no parquimetro public double getSaldo() { }
// Emite um ticket de 2 reais se houver saldo disponível
// Retorna true se a operação foi possível
public bool emiteTicket() { }
// Devolve o saldo existente
// Retorna o valor devolvido
public int devolve() { }
}
public class ParquimetroTest {
private Parquimetro parq;
@Before
public void setUp() throws Exception {
parq = new Parquimetro();
parq.insereMoeda(100);
}
@Test public void testInsereMoeda() {
parq.insereMoeda(1);
parq.insereMoeda(5);
parq.insereMoeda(10);
parq.insereMoeda(25);
parq.insereMoeda(50);
parq.insereMoeda(100);
assertEquals(291, parq.getSaldo());
}
@Test
public void testGetSaldo() {
int actual = parq.getSaldo();
assertEquals(100, actual);
}
@Test
public void testEmiteTicket() {
parq.insereMoeda(50);
parq.insereMoeda(50);
parq.insereMoeda(100);
boolean actual = parq.emiteTicket();
assertEquals(true, actual);
}
@Test public void testDevolve() {
parq.insereMoeda(50);
parq.insereMoeda(50);
parq.insereMoeda(100);
parq.emiteTicket();
int actual = parq.devolve();
assertEquals(100, actual);
}
}
public class Parquimetro {
private int saldo;
public Parquimetro() {
saldo = 0;
}
public void insereMoeda(int valor) {
switch (valor) {
case 1:
case 5:
case 10:
case 25:
case 50:
case 100:
saldo += valor;
break;
default:
System.out.println("Erro!");
}
}
public int getSaldo() { return saldo; }
public boolean emiteTicket() {
if (getSaldo() < 2) { return false; }
saldo -= 2;
return true;
}
public int devolve() {
int tmp = saldo;
saldo = 0;
return tmp;
}
}
emiteTicket
subtrai o valor 2 do saldoEntão o correto é subtrair 200!
public bool emiteTicket() {
if (saldo < 200) return false;
saldo -= 200;
return true;
}
Veja também que o teste específico criado para o método emiteTicket
era fraco e não detectou o erro!
O JUnit 4 oferece uma série de recursos adicionais para facilitar a implementação dos casos de teste
Pode-se verificar se um método não está ultrapassando o tempo de execução previsto para ele:
@Test(timeout=500)
public void testCalculoDemorado() {
calc.calculoDemorado();
assertEquals(55,calc.get());
}
O método assertEquals
está sobrecarregado para trabalhar com arrays
void assertEquals(Object[] esp, Object[] real);
Dois arrays são considerados iguais se o conteúdo de todas as suas posições for igual
...
@Test
public void testSubtrai1() {
calc.subtrai(3);
assertEquals(6,calc.get());
}
@Test
public void testSubtrai2() {
calc.subtrai(-3);
assertEquals(12,calc.get());
}
@Test
public void testSubtrai3() {
calc.subtrai(0);
assertEquals(9,calc.get());
}
...
Para criar testes parametrizados deve-se usar um executor (runner) diferente do executor padrão do Junit
Deve-se usar o executor: Parameterized.class
Para indicar o uso de um executor diferente usa-se a anotação @RunWith
No caso específico usa-se:
@RunWith(Parameterized.class)
O executor Parameterized exige uma classe com duas características:
@Parameters
que retorne uma instância da classe Collections, contendo os valores a serem usados nos testesOs testes propriamente ditos devem fazer acesso a esses parâmetros
@RunWith(Parameterized.class) // especificação do runner
public class TesteParametrizado {
private static Calculadora calc;
private int param;
private int result;
// Método que retorna coleção com pares valor/resultado esperado
@Parameters
public static Collection data() {
return Arrays.asList(new Object[][]{ {10, 0}, {9, 1}, {11, -1}, {4, -5} });
}
...
...
// Relaciona cada par da coleção com as variáveis param e result
public TesteParametrizado(int param, int result) {
this.param = param;
this.result = result;
}
@Before
public void inicializaTeste() {
calc = new Calculadora(10);
}
// Este método será ativado 4 vezes, uma para cada posição da coleção
@Test
public void subtracoes() {
calc.subtrai(param);
assertEquals(result, calc.get());
}
}
Quando o conjunto de casos de teste é muito grande, pode ocorrer de nem todos serem implementados ao mesmo tempo
Se um caso de teste está previsto mas ainda não foi implementado, pode-se usar a anotação @Ignore
antes de @Test
Vantagem: o caso de teste aparece no relatório como ignorado!
@Ignore("Not ready yet!!")
@Test
public void testSoma() {
System.out.println("Not yet implemented");
}
Deixar em comentário pode levar a esquecimentos se o conjunto de casos de teste for muito grande!
DELAMARO, M. E.; MALDONADO, J. C.; JINO, M. Introdução ao teste de software. (Capítulos 1 e 2)
PEZZÉ, M. Teste e análise de software: processo, princípios e técnicas. (Capítulo 15)