Testes de Unidade com JUnit
Projeto de Sistemas
Roberto A. Bittencourt
Material preparado com base nas aulas de Martin Stepp. Usa materiais de M. Ernst, S. Reges, D. Notkin, R. Mercer, Wikipedia
Bugs e Testes
• Bugs são inevitáveis em qualquer software complexo.
– Estimativas da indústria: 10-50 bugs por 1000 linhas de
código.
– Um bug pode ser visível ou pode se esconder em seu código
até bem mais tarde.
• Testes: Tentativa sistemática de revelar erros.
– Teste falhou: um erro foi demonstrado.
– Teste passou: erros não foram encontrados (nesta situação
particular).
Testes de Unidade
• Teste de unidade: Procura erros em um subsistema isolado.
– Geralmente, um “subsistema" significa um arquivo, uma classe ou
objeto em particular.
– A biblioteca Java JUnit ajuda a realizar testes de unidade
facilmente.
• Ideia básica:
– Para uma dada classe Foo, crie outra classe FooTest para
testá-la, contendo vários métodos que são “casos de teste”.
– Cada método procura resultados específicos e passa (ou falha).
• JUnit provê comandos "assert" para ajudar a escrever testes.
– A ideia: Coloque chamadas de asserções em seus métodos de
teste para checar coisas que você espera serem verdadeiras. Se
não forem, o teste vai falhar.
JUnit e Eclipse
• Para adicionar JUnit a um projeto Eclipse, clique:
– Project Properties Build Path Libraries
Add Library... JUnit JUnit 4 Finish
• Para criar um caso
de teste:
– Botão direito num arquivo
e escolha New Test Case
– Ou clique File New
JUnit Test Case
– Eclipse pode criar stubs
Uma classe de teste JUnit
import org.junit.*;
import static org.junit.Assert.*;
public class Name {
...
@Test
public void name() {
// um método caso de teste
...
}
}
– Um método com @Test é marcado como um caso de teste JUnit.
• Todos os métodos @Test rodam quando JUnit roda a sua classe de
teste.
Métodos de asserção JUnit
• Cada método pode receber também uma string a ser
mostrada, caso ele falhe:
– e.g. assertEquals("message", expected, actual)
– Por que não há método pass?
assertTrue(test) Falha se o teste booleano for false assertFalse(test) Falha se o teste booleano for true assertEquals(expected, actual) Falha se os valores não forem iguais
assertSame(expected, actual) Falha se os valores não forem os mesmos
(por ==)
assertNotSame(expected, actual) Falha se os valores forem os mesmos
(por ==)
assertNull(value) falha se o valor dado for não null
assertNotNull(value) Falha se o valor for null
Teste JUnit de uma ArrayIntList
import org.junit.*;import static org.junit.Assert.*;
public class TestArrayIntList {
@Test
public void testAddGet1() {
ArrayIntList list = new ArrayIntList(); list.add(42); list.add(-3); list.add(15); assertEquals(42, list.get(0)); assertEquals(-3, list.get(1)); assertEquals(15, list.get(2)); } @Test
public void testIsEmpty() {
ArrayIntList list = new ArrayIntList(); assertTrue(list.isEmpty()); list.add(123); assertFalse(list.isEmpty()); list.remove(0); assertTrue(list.isEmpty()); } ...
Rodando um teste
• Botão direito nele no Package Explorer do Eclipse à
esquerda; e então escolha:
Run As JUnit Test
• A barra do JUnit vai mostrar
verde se todos os testes passarem,
vermelho
se algum teste falhar.
• O Failure Trace mostra que testes
falharam, se algum falhou,
Exercício com JUnit
Dada uma classe
Date com os seguintes métodos:
– public Date(int year, int month, int day) – public Date() // hoje
– public int getDay(), getMonth(), getYear()
– public void addDays(int days) // avance por days
– public int daysInMonth()
– public String dayOfWeek() // e.g. “Sunday"
– public boolean equals(Object o)
– public boolean isLeapYear() // é ano bissexto
– public void nextDay() // avance um dia
– public String toString()
• Escreva testes de unidade para checar o seguinte:
– Que nenhum objeto Date alcance um estado inválido.
– Que o método addDays funcione apropriadamente.
• Ele deve ser eficiente o bastante para adicionar 1.000.000 de dias em
apenas uma chamada.
O que há de errado aqui?
public class DateTest { @Test
public void test1() {
Date d = new Date(2050, 2, 15); d.addDays(4); assertEquals(d.getYear(), 2050); assertEquals(d.getMonth(), 2); assertEquals(d.getDay(), 19); } @Test
public void test2() {
Date d = new Date(2050, 2, 15); d.addDays(14); assertEquals(d.getYear(), 2050); assertEquals(d.getMonth(), 3); assertEquals(d.getDay(), 1); } }
Asserções bem estruturadas
public class DateTest { @Test
public void test1() {
Date d = new Date(2050, 2, 15); d.addDays(4);
assertEquals(2050, d.getYear()); // valor esperado
assertEquals(2, d.getMonth()); // deve estar
assertEquals(19, d.getDay()); // à ESQUERDA
}
@Test
public void test2() {
Date d = new Date(2050, 2, 15); d.addDays(14);
assertEquals(“ano após +14 dias", 2050, d.getYear()); assertEquals(“mês após +14 dias", 3, d.getMonth()); assertEquals(“dia após +14 dias", 1, d.getDay()); } // casos de teste normalmente devem ter mensagens
} // explicando o que está sendo checado, para // melhor compreensão da falha
Objetos de resposta esperada
public class DateTest { @Test
public void test1() {
Date d = new Date(2050, 2, 15); d.addDays(4);
Date expected = new Date(2050, 2, 19);
assertEquals(expected, d); // use um objeto de resposta
} // esperada para minimizar
// testes
// (Date deve ter os métodos
@Test // toString e equals)
public void test2() {
Date d = new Date(2050, 2, 15); d.addDays(14);
Date expected = new Date(2050, 3, 1);
assertEquals(“data após +14 dias", expected, d); }
Nomeando casos de teste
public class DateTest { @Test
public void test_addDays_withinSameMonth_1() { Date actual = new Date(2050, 2, 15);
actual.addDays(4);
Date expected = new Date(2050, 2, 19);
assertEquals("date after +4 days", expected, actual); }
// dê aos métodos de casos de tese nomes descritivos // realmente longos
@Test
public void test_addDays_wrapToNextMonth_2() { Date actual = new Date(2050, 2, 15);
actual.addDays(14);
Date expected = new Date(2050, 3, 1);
assertEquals("date after +14 days", expected, actual); }
// dê nomes descritivos aos valores esperados/reais
O que há de errado aqui?
public class DateTest { @Test
public void test_addDays_addJustOneDay_1() { Date actual = new Date(2050, 2, 15);
actual.addDays(1);
Date expected = new Date(2050, 2, 16); assertEquals(
“deveria ser " + expected + "\n" +
" mas em vez disso foi " + actual\n", expected, actual);
} ... }
Boas mensagens de asserção
public class DateTest { @Test
public void test_addDays_addJustOneDay_1() { Date actual = new Date(2050, 2, 15);
actual.addDays(1);
Date expected = new Date(2050, 2, 16);
assertEquals(“adicionando um dia a 2050/2/15", expected, actual);
} ... }
// JUnit já irá mostrar os // valores esperados e reais // em sua saída;
//
// não precisa repeti-los // na mensagem de asserção
setUp e tearDown
@Before
public void name() { ... }
@After
public void name() { ... }
– Métodos que rodam antes/depois de cada caso de teste
ser chamado
@BeforeClass
public
static
void name() { ... }
@AfterClass
public
static
void name() { ... }
– Métodos que rodam apenas uma vez antes/depois da
classe de teste inteira rodar
Dicas para testes
• Você não pode testar toda entrada possível, valor de
parâmetro, etc.
– Portanto, você deve pensar num conjunto limitado de testes para
expor prováveis bugs.
• Pense em casos-limite
– Números positivos; zero; negativos
– Exatamente no limite do tamanho de uma array ou coleção
• Pense em casos vazios e casos de erro
– 0, -1, null; uma lista ou array vazia
• Teste comportamento combinado
– Talvez add funcione normalmente , mas falhe depois de você
chamar
remove
– Faça múltiplas chamadas; talvez size falhe apenas na segunda
vez
O que há de errado aqui?
public class DateTest {
// teste todos os dias do ano
@Test(timeout = 10000)
public void tortureTest() {
Date date = new Date(2050, 1, 1); int month = 1;
int day = 1;
for (int i = 1; i < 365; i++) { date.addDays(1);
if (day < DAYS_PER_MONTH[month]) {day++;}
else {month++; day=1;} assertEquals(new Date(2050, month, day), date); }
}
private static final int[] DAYS_PER_MONTH = {
0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; // Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
Testes fidedignos
• Teste uma coisa por vez para cada método de teste.
– 10 testes pequenos são muito melhores que um teste 10x maior.
• Cada método de teste devia ter poucas (talvez uma)
instruções de asserção.
– Se você fizer asserções demais, a primeira que falhar, pára o teste.
– Você não vai saber se uma asserção posterior teria falhado.
• Testes devem evitar muita lógica.
– Minimize if/else, loops, switch, etc.
• Testes tortura são ok, mas somente em adição a testes
simples.
Desenvolvimento dirigido por testes
• Testes de unidade podem ser escritos depois, durante ou
mesmo antes de codificar.
– test-driven development (TDD): escreva os testes, só depois
escreva o código para passar nos testes.
• Imagine que nós gostaríamos de adicionar um método
subtractWeeks a nossa classe Date, que desloca
esta
Date para trás no tempo por um dado número de
semanas.
• Escreva o código para testar este método antes que ele
tenha sido escrito.
– Então, assim que implementarmos o método, saberemos se
ele funciona.
Suítes de teste
• Suíte de teste: Uma classe que roda vários testes
JUnit.
– Um modo fácil de rodar todos os testes de sua aplicação
de uma só vez.
import org.junit.runner.*;
import org.junit.runners.*;
@RunWith(Suite.class)
@Suite.SuiteClasses({
TestCaseName1.class,
TestCaseName2.class,
...
TestCaseNameN.class,
})
Exemplo de suíte de testes
import org.junit.runner.*;
import org.junit.runners.*;
@RunWith(Suite.class)
@Suite.SuiteClasses({
WeekdayTest.class,
TimeTest.class,
CourseTest.class,
ScheduleTest.class,
CourseComparatorsTest.class
})
Sumário - JUnit
• Testes necessitam de atomicidade de falha (habilidade de saber
exatamente o que falhou).
– Cada teste deve ter um nome claro, longo e descritivo.
– Asserções devem ter sempre mensagens claras para saber o que falhou.
– Escreva muitos testes pequenos, e não um grande teste.
• Cada teste devia ter , grosso modo, apenas uma asserção em seu final.