Combinando Test Driven Development (TDD) com Design By Contract (DBC) João Arthur Brunet Monteiro Universidade Federal de Campina Grande - UFCG Centro de Engenharia Elétrica e Informática - CEEI Departamento de Sistemas e Computação - DSC Roteiro Introdução Backgroud Test Driven Development Design By Contract Combining Formal Specifications with Test Driven Development Visão Geral Técnica Proposta Resultados Discussões Introdução Combining Formal Specifications with Test Driven Development Design By Contract e Test Driven Development Trabalho relativamente recente (2004). Por quê? Por quê combinar as duas técnicas? Test Driven Development Técnica de desenvolvimento de software baseada em testes Testes são desenvolvidos antes do código Técnica ágil Pequenos passos (baby steps) Manter a simplicidade (KISS) Test Driven Development Não constitui somente uma maneira de testar. TDD é uma técnica desenvolvida que, ao aplicada, força o programador a pensar no design do código antes dele ser implementado. Passos: Escrever o teste Obter red bar Escrever o código mais simples que passe no teste Obter green bar Refatorar o código Testes Descrevem parcialmente, através de exemplos, o comportamento do sistema Usados como documentação de um programa Aumentam o entendimento sobre o código Aumentam a qualidade do código Mas... Não generalizam propriedades do sistema Design By Contract Metodologia que se baseia na construção de contratos entre partes do software. Usa especificações formais para estabelecer contratos entre partes do software pré-condições, pós-condições e invariantes Especificações formais são também usadas como documentação sem fazer referência ao código. Por isso, aumentam a reusabilidade do software e, consequentemente, sua qualidade Especificações formais descrevem o comportamento do sistema de maneira mais completa que testes o fazem Mas... A tarefa de mostrar que um software está de acordo com as especificações é geralmente muito complexa Por quê não? Por quê não combinar as técnicas? Combining Formal Specifications with Test Driven Development Objetivos Combinar especificação formal com TDD Não perder a agilidade de TDD Ganhar com as generalizações das especificações formais Técnica Proposta Os testes usados para conduzir o desenvolvimento do código também são usados para conduzir o desenvolvimento das especificações Especificações generalizam os testes Asserções são geradas a partir das especificações afim de checar inconsistências entre testes, código e especificações Três Variáveis de interesse Testes Código Especificações Manter a consistência entre as variáveis aumenta a qualidade do código desenvolvido e da documentação uma vez que as especificações formais não dão margem a interpretações ambíguas Tecnologias Utilizadas Junit Código + Java Testes + JML Especificações Tecnologias utilizadas Junit e Java dispensam apresentações! Java Model Language (JML) é uma linguagem para formulações de pré-condições, pós-condições e invariantes Especificações escritas em JML são simples de entender JML vem com um checador de asserções em tempo de execução As tecnologias foram usadas para exemplificar a técnica, no entanto, a mesma não exige tecnologias específicas! Técnica Proposta Consiste em, ao estilo TDD, uma sequência de passos que se repetem ao longo do desenvolvimento do software. 1. 2. 3. 4. 5. Escrever o teste Implementar o código Refatorar o código Generalizar os testes para especificações Refatorar as especificações Aplicando a técnica Exemplo usado: Programa que, dado um inteiro n, retorna os números primos até n. Passo 1: Escrever o teste public void testZero() { List primes Primes.primes(0); assertTrue(primes.isEmpty()); } Passo 2: Escrever o código public static List primes(int n) { return new LinkedList(); } Passo 3: Refatorar o código Aplicando a técnica Passo 4: Generalizar o teste para especificações public bahavior requires n<=1; ensures \result.isEmpty(); } Veja que o caso de n ser igual a 1 foi coberto pela especificação, mas não pelo teste. Analisando a especificação é possível notar que falta um teste. No entanto, a inclusão do novo teste não irá apontar faltas no código. Aplicando a técnica Passo 1: Escrever o teste public void testTwo() { List primes = new Primes.primes(2); assertEquals(1, primes.size()); assertTrue( primes.contains(new Integer(2))); } Passo 2: Escrever o código public static List primes(int n) { List primes = new LinkedList(); if (n==2) primes.add(new Integer(2)); return primes; } Aplicando a técnica Passo 4: : Generalizar o teste para especificações public bahavior requires n<=1; ensures \result.isEmpty(); also requires n ==2; ensures \result.size() == 1 && \result.contains(new Integer(2)); Passo 1: Escrever o teste public void testLosts1() { ... for (int i = 1; i<=n ; i++) { if (isPrime(i)) assertTrue(primes.contains(new Integer(i))) } } Aplicando a técnica Especificação da função isPrime(int i) public bahavior ensures \result < = = > (n > 1 && (\forall int i; i > 1 && i < n; n % i != 0)); Passo 2: Escrever o código public static List primes(int n) { List primes = new LinkedList(); for (int i = 2; i <= n; i++) primes.add(new Integer(i)); return primes; } Aplicando a técnica Passo 4: : Generalizar o teste para especificações public bahavior requires n <= 1; ensures \result.isEmpty(); also requires n == 2; ensures \result.size() == 1 && \result.contains(new Integer(2)); also requires n > 2; ensures (\forall int i; i >= 2 && i <= n; isPrime(i) == > \result.contains(new Integer(i))); Aplicando a técnica A especificação diz que, para um dado n, a função retorna uma lista contendo os números primos de 2 até n. Porém, não assegura que a lista só contém os números primos de 2 a n. Passo 1: Escrever o teste public void testLosts1() { List primes = Primes.primes(1000); ... for (Integer i : primes) { assertTrue(isPrime(i)); assertTrue(prime <= n); } } Aplicando a técnica Passo 2 : Escrever o código ... (crivo de Eratóstenes) Passo 3: Refatorar o código ... Passo 4: : Generalizar o teste para especificações Aplicando a técnica public bahavior requires n <= 1; ensures \result.isEmpty(); also requires n == 2; ensures \result.size() == 1 && \result.contains(new Integer(2)); also requires n > 2; ensures (\forall int i; i >= 2 && i <= n; isPrime(i) == > \result.contains(new Integer(i))); also requires n > 2; ensure (\forall Integer i; \result.contains(i)); isPrime(i); Aplicando a técnica Em TDD é argumentado que testes não devem ser refatorados. Refatorar especificações tornam-as mais claras e concisas. Passo 5: Refatorar especificações public bahavior ensures (\forall int i; i >= 2 && i <= n; isPrime(i) == > \result.contains(new Integer(i))); && (\forall Integer i; \result.contains(i); isPrime(i); Observações Rodando os testes instrumentados com as asserções geradas pelas especificações em JML, é possível garantir que a especificação não entra em conflito com a implementação do código O caso de haver números maiores que n não foi considerado. Mas neste caso, basta adicionar na especificação que i deve ser menor que n Resultados Como resultado do processo temos um conjunto de testes, código e especificações que assegura que o código implementa o comportamento esperado, documentado pela especificação As especificações podem ser usadas como documentação para descrever o comportamento do programa sem fazer referência ao código fonte Note que a especificação é mais simples de entender do que os testes e o código O poder de abstração aumenta com o uso das especificações Resultados Bugs que não eram encontrados fazendo uso de testes, são revelados quando a técnica é usada O uso da técnica ajuda a aumentar o conjunto de testes Assim como testes, não garante a ausência de bugs Discussão Dúvidas? Referências Hubert Baumeister. Combining Formal Specifications with Test Driven Development. XP/Agile Universe 2004: 1-12 Bertrand Meyer: Applying "Design by Contract, in Computer (IEEE), vol. 25, no. 10, October 1992, pages 4051. BECK, K. and FOWLER, M. (2001). Planning Extreme Programming. Addison Wesley.