Exemplo de desenvolvimento com testes Prof. Dr. Alfredo Goldman Departamento de Ciência da Computação IME / USP 3 de Abril de 2003 VI Semana da Computação Problema com testes Todos sabem: devem ser escritos; Poucos o fazem, e por quê não ? Estou com muita pressa Mas, isto cria um círculo vicioso: menos testes menos produtividade menos estabilidade mais pressão 03/04/2003 VI Semana da Computação Como quebrar este ciclo Criando um ambiente simples de testes Depois de fazer os primeiros testes o hábito vem para ficar Vamos mostrar como seria um desenvolvimento ideal.... Através do JUnit... Mas, antes uma visão geral: 03/04/2003 VI Semana da Computação JUnit - como funciona ? Arcabouço Java para testes de unidade API para construir testes Classes básicas: Test, TestCase, TestSuite,... Usam-se métodos tipo premissa: • assertTrue(), assertEquals, fail(), ... API para a execução de testes (TestRunners) Modo texto Modo gráfico 03/04/2003 VI Semana da Computação Junit - origens e uso Criado por K. Beck e E. Gamma Padrão para testes em Java Permite a execução automática de testes Executa os testes de forma silenciosa Dirigido a testes de unidade Métodos Pode-se agrupar diversos testes 03/04/2003 VI Semana da Computação Exemplo de uso Padrão use o método testXxx() para testar o método xxx() Utilize os métodos da classe TestCase assertEquals( objeto1, objeto2); assertTrue( varBool); assertNotNull( objeto); fail(); 03/04/2003 VI Semana da Computação JUnit na prática 1) O TestRunner recebe uma subclasse de TestCase; 2) Descobre seus métodos (reflexão); 3) Para cada testXxx(); cria nova instância garante independência !! executa setUp(); testXxx(); tearDown(); 4) Possibilidades: término OK, falha, ou exceção 03/04/2003 VI Semana da Computação Criando uma seqüência de testes Classe TestSuite Método addTest adiciona um teste a lista Encontra os testes em uma classe (reflexão) new TestSuite( ClasseDeTestes.class); Pode-se juntar tudo TestSuite ts = new TestSuite(“tudo”); ts.addTest( pacote.Teste1.testeX); ts.addTest( ClasseDeTestes.suite()); 03/04/2003 VI Semana da Computação Premissas em Java (JDK 1.4) Nova palavra chave: assert assert (n > 0) : “n não é positivo”; Podem ser desligadas facilmente Provocam um AssertionError quando falham Para usá-las: javac -source 1.4 Classe.java java -ea Classe 03/04/2003 VI Semana da Computação JUnit e premissas: Premissas são usadas dentro do código Os testes JUnit ficam em classes separadas Não tem acesso a partes encapsuladas JUnit testa a partir da interface Premissas podem verificar lógica interna 03/04/2003 VI Semana da Computação Testes de desempenho JUnitPerf Métodos para medir desempenho e escalabilidade TimedTest Mede, e limita o tempo do teste LoadTest Execução concorrente, configuração por timers ThreadTest Executa o teste como uma thread separada 03/04/2003 VI Semana da Computação Testes de Stress JMeter - testa nos limites De carga Para diferentes tipos BDs, páginas WEB, objetos Java Gera gráficos Pode ser usado em conjunto com o JUnitPerf 03/04/2003 VI Semana da Computação Testes de páginas WEB Testar do ponto de vista do usuário Através de páginas Testes funcionais Extensões do Junit HttpUnit e ServletUnit JXweb (especifica os testes em XML) XMLUnit Todos são projetos sourceforge 03/04/2003 VI Semana da Computação JUnit na prática: O programa Um sistema para representar diversas moedas; Para começar: algo simples. 03/04/2003 VI Semana da Computação class Money { private int fAmount; private String fCurrency; public Money(int amount, String currency) { fAmount = amount; fCurrency = currency; } public int amount() { return fAmount; } public String currency() { return fCurrency; } } Para somar dois “Moneys” da mesma moeda (currency): public Money add(Money m) { return new Money(amount()+m.amount(), currency()); } 03/04/2003 VI Semana da Computação Questão de hábito Code a little, test a little, code a little, test a little.... Já temos um objeto, vamos testá-lo !! No JUnit os testes devem ser subclasses de TestCase 03/04/2003 VI Semana da Computação public class MoneyTest extends TestCase { //… public void testSimpleAdd() { Money m12CHF= new Money(12, "CHF"); // (1) Money m14CHF= new Money(14, "CHF"); Money expected= new Money(26, "CHF"); Money result= m12CHF.add(m14CHF); // (2) Assert.assertTrue(expected.equals(result)); // (3) } } O testSimpleAdd() consiste em: Código para criar os objetos; Código para usar os objetos; Código para verificar os resultados. Falta fazer a sobrecarga de equals Mas antes um teste para o equals public void testEquals() { Money m12CHF= new Money(12, "CHF"); Money m14CHF= new Money(14, "CHF"); Assert.assertTrue(!m12CHF.equals(null)); Assert.assertEquals(m12CHF, m12CHF); Assert.assertEquals(m12CHF, new Money(12, "CHF")); // (1) Assert.assertTrue(!m12CHF.equals(m14CHF)); } // lembrete: o equals do object volta true se os // objetos comparados são o mesmo. Agora sim public boolean equals(Object anObject) { if (anObject instanceof Money) { Money aMoney = (Money) anObject; return aMoney.currency().equals(currency()) && amount() == aMoney.amount(); } return false; } // faltou sobrecarregar o método hashCode... 03/04/2003 VI Semana da Computação Mas, já apesar dos testes serem pequenos já há código duplicado... public class MoneyTest extends TestCase { private Money f12CHF; private Money f14CHF; protected void setUp() { f12CHF= new Money(12, "CHF"); f14CHF= new Money(14, "CHF"); } } 03/04/2003 VI Semana da Computação Agora os testes podem ser rescritos como: public void testEquals() { assert(!f12CHF.equals(null)); assertEquals(f12CHF, f12CHF); assertEquals(f12CHF, new Money(12, "CHF")); assert(!f12CHF.equals(f14CHF)); } public void testSimpleAdd() { Money expected= new Money(26, "CHF"); Money result= f12CHF.add(f14CHF); assert(expected.equals(result)); } 03/04/2003 VI Semana da Computação Próximos passos Definir como rodar um teste individual; Definir como rodar uma seqüência de testes. 03/04/2003 VI Semana da Computação // forma estática, com classe interior TestCase test= new MoneyTest("simple add") { public void runTest() { testSimpleAdd(); } }; // forma dinâmica, usa reflexão TestCase test= new MoneyTest("testSimpleAdd"); Pode-se automatizar testes Criando uma seqüência de testes public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest(new MoneyTest("testEquals")); suite.addTest(new MoneyTest("testSimpleAdd")); return suite; } 03/04/2003 VI Semana da Computação Pode-se automatizar testes Ou apenas: public static Test suite() { return new TestSuite(MoneyTest.class); } Agora, um pouco de JUnit na prática. 03/04/2003 VI Semana da Computação Continuando o projeto Deve-se poder guardar diversos tipos de moeda class MoneyBag { private Vector fMoneis= new Vector(); MoneyBag(Money m1, Money m2) { appendMoney(m1); appendMoney(m2); } MoneyBag(Money bag[]) { for (int i= 0; i < bag.length; i++) appendMoney(bag[i]); } } 03/04/2003 VI Semana da Computação Para os testes deve se criar também objetos do novo tipo protected void setUp() { f12CHF= new Money(12, "CHF"); f14CHF= new Money(14, "CHF"); f7USD= new Money( 7, "USD"); f21USD= new Money(21, "USD"); fMB1= new MoneyBag(f12CHF, f7USD); fMB2= new MoneyBag(f14CHF, f21USD); } Devem se criar novos testes, mas os testes antigos continuam lá public void testBagEquals() { assert(!fMB1.equals(null)); assertEquals(fMB1, fMB1); assert(!fMB1.equals(f12CHF)); assert(!f12CHF.equals(fMB1)); assert(!fMB1.equals(fMB2)); } E devem continuar funcionando... Agora podemos melhorar o método add public Money add(Money m) { if (m.currency().equals(currency()) ) return new Money(amount() + m.amount(), currency()); return new MoneyBag(this, m); } // ops MoneyBag != Money.... Agora existem duas representações de dinheiro... interface IMoney { public abstract IMoney add(IMoney aMoney); //… } Mas, ainda não temos testes para tipos mistos... public void testMixedSimpleAdd() { // [12 CHF] + [7 USD] == {[12 CHF][7 USD]} Money bag[]= { f12CHF, f7USD }; MoneyBag expected= new MoneyBag(bag); assertEquals(expected, f12CHF.add(f7USD)); } Os outros testes seguem o mesmo padrão: testBagSimpleAdd - soma MoneyBag com Money testSimpleBagAdd - soma Money com MoneyBag testBagBagAdd - soma dois MoneyBags Mais testes estão disponíveis: public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest(new MoneyTest("testMoneyEquals")); suite.addTest(new MoneyTest("testBagEquals")); suite.addTest(new MoneyTest("testSimpleAdd")); suite.addTest(new MoneyTest("testMixedSimpleAdd")); suite.addTest(new MoneyTest("testBagSimpleAdd")); suite.addTest(new MoneyTest("testSimpleBagAdd")); suite.addTest(new MoneyTest("testBagBagAdd")); return suite; } Agora sim vamos implementá-los... class Money implements IMoney { public IMoney add(IMoney m) { return m.addMoney(this); } //… } class MoneyBag implements IMoney { public IMoney MoneyBag.add(IMoney m) { return m.addMoneyBag(this); } //… } //… IMoney addMoney(Money aMoney); IMoney addMoneyBag(MoneyBag aMoneyBag); } Em Money. public IMoney addMoney(Money m) { if (m.currency().equals(currency())) return new Money(amount()+m.amount(), currency()); return new MoneyBag(this, m); } public IMoney addMoneyBag(MoneyBag s) { return s.addMoney(this); } Em MoneyBag. public IMoney addMoney(Money m) { return new MoneyBag(m, this); } public IMoney addMoneyBag(MoneyBag s) { return new MoneyBag(s, this); } Surge um problema.... E se retira-se 12CHF de um MoneyBag com 12CHF ??? Primeiro o teste... public void testSimplify() { // {[12 CHF][7 USD]} + [-12 CHF] == [7 USD] Money expected= new Money(7, "USD"); assertEquals(expected, fMS1.add(new Money(-12, "CHF"))); } Depois o código. public IMoney addMoney(Money m) { return (new MoneyBag(m, this)).simplify(); } public IMoney addMoneyBag(MoneyBag s) { return (new MoneyBag(s, this)).simplify(); } private IMoney simplify() { if (fMonies.size() == 1) return (IMoney)fMonies.firstElement() return this; } Desenvolvimento com testes Testes devem ser escritos assim que possível; Testes devem ser adaptados segundo as mudanças; Deixe os testes antigos rodando; Quando surgem novas idéias (simplify), crie testes, veja se funcionam, e se necessário altere o código. 03/04/2003 VI Semana da Computação