Introdução ao TDD (Test-Driven Development)

Instrutor Cristian

Sobre mim

  • Me chamo Cristian, desenvolvedor há 7 anos
  • Formado na Universidade Vila Velha (UVV) em Ciência da Computação
  • Freelancer
  • Consultoria em banco
  • Java, Spring Boot, ...
  • Contato: cristianc.mello@gmail.com
Instrutor Cristian

Agradecimentos

  • Agradeço a Deus e minha família
  • Aos professores Saulo e Susileia por terem me dado esta oportunidade e terem possibilitado os recursos necessários!
Instrutor Cristian

Agradeço a você que está podendo receber este conteúdo!

Instrutor Cristian

Como fiz estes slides?

Marpit

O Marpit pode ser adicionado ao VSCODE. Podemos exportar Markdown como PDF, PowerPoint e HTML. Acesse a documentação aqui. Dá para adicionar fórmulas matemáticas do Latex.

Seria fantástico isso para apresentação de um TCC, montar aulas e etc.

Instrutor Cristian

alt text

Instrutor Cristian

As ideias deste curso

Trazer uma prática de programação Agile. Sim, o Ágil tem uma disciplina pouco comentada.

Ter um referencial técnico mais refinado sobre metodologias de codificação e de teste

Ajudar a lidar com a ansiedade ao programar com base em disciplinas de Engenharia de Software atestadas mundialmente

Instrutor Cristian

Quem não fica ansioso ao programar?

Como lidar?

Com conhecimento sobre técnicas de programação e de Engenharia de Software.

Conhecer alguma literatura de referência é de suma importância

  • Para entrevistas técnicas
  • Code Review
  • Elaboração de materiais técnicos e publicações
  • Claro, para programar melhor!
Instrutor Cristian

Tópicos a serem abordados

Unidade I

  • Conceitos do Desenvolvimento Orientado a Testes (TDD)
  • Implementação do TAD Pilha com TDD
  • Contexto adequado da prática do Clean Code
  • Conhecer como funciona o fluxo de trabalho com uma framework de teste como JUnit
  • Cobertura de código (ou cobertura de teste)
  • Fenômeno da Generalidade e Especificidade
Instrutor Cristian
  • Visão geral das abordagens da programação
  • Introdução da aplicação formal do Anamorfismo e Catamorfismo
  • Exercícios no fim desta Unidade
Instrutor Cristian

Unidade II

  • Introdução ao Clean Craftsmanship
  • Papel do TDD na Engenharia de Software
  • Origem do Design Orientado a Objeto e seu papel na indústria e no surgimento das IDEs (Integrated Development Environment)
  • TDD nasceu na NASA: projeto Mercury
  • Análise da entrevista do Fábio Akita com Robert C. Martin, um dos autores do Agile e Clean Code
Instrutor Cristian

Unidade III

  • TDD Avançado
  • Níveis de teste (Unitário, Integração e Aceitação)
  • Pilha com suporte a Java Generics e Overflow
  • Integração Contínua e DevOps
  • Conceitos de Acoplamento e Coesão
  • TDD num projeto web com Spring Boot
  • Desenho de Arquitetura com PlantUML
  • Implementando uma API RESTful com Banco de Dados em SQL
Instrutor Cristian

Referências bibliográficas (Unidade I)

  1. Tesson, J., Hashimoto, H., Hu, Z., Loulergue, F., Takeichi, M. (2011). Program Calculation in Coq. In: Johnson, M., Pavlovic, D. (eds) Algebraic Methodology and Software Technology. AMAST 2010. Lecture Notes in Computer Science, vol 6486. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-642-17796-5_10

  2. Martin, Robert C. Clean Craftsmanship: Disciplines, Standards, and Ethics. Robert C. Martin Series. Boston, MA: Addison-Wesley, 2021.

  3. Martin, Robert C., and Robert C. Martin. Clean Architecture: A Craftsman's Guide to Software Structure and Design. Prentice Hall, 2018.

  4. Beck, Kent. Test-Driven Development: By Example. The Addison-Wesley Signature Series. Boston: Addison-Wesley, 2003.

Instrutor Cristian
  1. Edmonds, Jeff. How to Think about Algorithms. Cambridge; New York: Cambridge University Press, 2008.

  2. Ramalho, Luciano. Fluent Python: Clear, Concise, and Effective Programming. Second edition. Beijing Boston Farnham Sebastopol Tokyo: O'Reilly, 2022.

Instrutor Cristian

Desenvolvimento Orientado a Testes (TDD - Test-Driven Development)

Instrutor Cristian
Instrutor Cristian

Primeira ponte de aço do mundo: Ponte de Eads (Mississipi, inaugurada em 1874)

  • As pontes eram feitas de ferro e madeira. Não suportavam o peso de trens de carga
  • James Eads projetou de uma forma totalmente inovadora para época

James testou os materiais antes de construir a ponte

  • Na época era costume esperar a ponte ser testada com trem passando de verdade. Muitos morreram. Outro teste comum era fazer um circo atravessar com vários elefantes.
  • Até hoje é uma das pontes mais duráveis do mundo!
Instrutor Cristian
Instrutor Cristian

Vamos direto ao ponto

Comumente o TDD é chamado de processo RED, GREEN e REFACTORING (OU BLUE). Iniciar o processo diretamente por essas etapas é desafiador e complicado. Entretanto, podemos considerar outros aspectos.

  • Não precisamos decorar as regras;
  • Aprende-se com a repetição;
  • Recomenda-se um instrutor para dar uma propulsão inicial.
  • De nada vale um exemplo completo salvo. TDD é uma prática a ser exercitada e não um livro de regras rígidas.
Instrutor Cristian

4 Regras do TDD: uma maneira mais bem definida

Robert C. Martin (conhecido como Uncle Bob) introduziu o conceito juntamente com o Kent Beck (responsável pela ideia do TDD como a conhecemos).

Instrutor Cristian

1ª REGRA: Escreva um teste que vai falhar por falta de código de produção.

Parece complicado, mas com a prática fica mais simples de encarar.

O que é código de produção?

O código que resolve problemas de compilação ou de falha dos testes é chamado de CÓDIGO DE PRODUÇÃO. É também o código que vai para o ambiente produtivo, ou seja, implantado.

Instrutor Cristian

2ª REGRA: Ao ver a falha de lógica ou de compilação acontecendo, resolva ambas escrevendo código de produção.

Evite de criar mais testes ao ver as falhas acontecendo.

Instrutor Cristian

3ª REGRA: Quando o teste falhar, escreva o código que o faz passar, mas que seja suficiente e não mais do que isso.

Precisa ser o código mais simples possível. Depois de aprovado, escreva mais testes.

Instrutor Cristian

4ª REGRA: Depois de fazer o código de teste passar, limpe a bagunça.

Aqui entra a ideia do Clean Code. Em resumo, um Clean Code (Código Limpo) adquire as seguintes qualidades (segundo Bob e Ron Jeffries):

  • Todos os testes passam;
  • Não existe duplicação;
  • Menor quantidade possível de classes, métodos, funções e etc.
    • Quanto mais código, mais custosa é a sua alteração;
    • Não deve existir código em desuso. Deve ser apagado.
Instrutor Cristian

Ciclo de Prática (Deixe este slide em destaque durante a prática)

Etapas:

  1. Escreva uma linha de código de teste que não vai compilar;
  2. Escreva uma linha de código de produção que faz o código de teste compilar;
  3. Escreva uma nova linha de código de teste que não compila;
  4. Escreva uma nova linha de código de produção que faz o teste compilar;
  5. Escreva 1 ou 2 linhas de código de teste que compila mas falha na asserção;
  6. Escreva 1 ou 2 linhas de código de produção que passa na asserção.
  7. Volte a Etapa 1.
Instrutor Cristian

Hands On: TAD de Pilha (Parte I)

Antes da prática, vamos revisar a teoria do TAD de Stack (Pilha).
TAD = Tipo Abstrato de Dados (parente mais antigo de uma classe em OOP).

Instrutor Cristian

Cenário simples: empilhando e consultando elemento removido usando a classe Stack (Java Collection)

Com jshell:

var stack = new Stack<Integer>() // alocar memoria ou instanciacao
stack.empty()  // verifica se esta vazia
stack.size()   // retorna a quantidade de elementos empilhados
stack.push(99) // empilha
stack.push(88)
stack.empty()  // indica que deixa de estar estar vazia
stack.size()   // indica 2 elementos empilhados
stack.pop()    // desempilha
stack.pop()

Obs.: A classe Stack usa os nomes size e empty para operações em vez de getSize e isEmpty. O resto do comportamento é igual.

Instrutor Cristian

Projeto em Java 21 com Maven

Precisamos de um boilerplate que suporte

  • Java 21 (JDK): a linguagem Java na versão 21 (lançada em 2023)
  • Maven 3: gerenciador de pacotes para baixar dependências
  • JUnit 5: framework para executar nossos testes. Será instalada no projeto pelo Maven.

O que é um boilerplate?

Instrutor Cristian

Vamos criar nosso ambiente no Codespaces

Instrutor Cristian
Instrutor Cristian

alt text

Instrutor Cristian

Instalando Extension Pack for Java

  • Pesquise por java
  • Ao ver Extension Pack for Java, clique em Install
  • Ao ser instalada, aparecerá Installed
Instrutor Cristian
Instrutor Cristian
Instrutor Cristian

Vamos clonar o boilerplate pronto no Codespaces

  • O exemplo já agiliza o processo de criação do primeiro teste
  • Espere o ambiente ser carregado. O terminal ficará habilitado.
Instrutor Cristian

alt text

Instrutor Cristian

Agora vamos clonar e acessar a pasta clonada

git clone https://github.com/xpian94/projeto-java-tdd.git
cd projeto-java-tdd
Instrutor Cristian

Reinicie a página para atualizar o ambiente

Instrutor Cristian

Observe que um 🚀 vai aparecer com o texto ao lado Java: Lightweight Mode

alt text

Clique para a Extensão do Java reconhecer os arquivos do projeto e assim facilitar as coisas para a gente.

Instrutor Cristian

Quando estiver escrito Java: Ready está tudo OK!

alt text

Instrutor Cristian

Entendendo alguns componentes comuns num projeto Maven

Instrutor Cristian

Arquivo POM.xml

POM (Project Object Model) contém informações para o Maven conseguir compilar o projeto, baixar dependências externas (chamamos de packages), automatizar processos (como a execução de testes) e etc.

Instrutor Cristian
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>project-java-tdd</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        ...
    </dependencies>
</project>
Instrutor Cristian

alt text

Instrutor Cristian

Podemos ver que:

  • O Maven precisa saber qual a versão de Java estamos a utilizar (21)
  • O Encoding de compilação é UTF-8
  • A versão do Modelo do POM
Instrutor Cristian

Dependência do JUnit 5

  • No exemplo projeto-java-tdd, já está adicionado:
<dependencies>
    <dependency>  
        <groupId>org.junit.jupiter</groupId>  
        <artifactId>junit-jupiter</artifactId>  
        <version>5.10.3</version>  
        <scope>test</scope>  
    </dependency>
</dependencies>
Instrutor Cristian

Verificando se o JUnit funciona

Executando todos os testes via Terminal

mvn clean test

Aparentemente funciona. Observa-se algo semelhante a seguir.

Instrutor Cristian
/workspaces/codespaces-blank/projeto-java-tdd (main) $
mvn clean test
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running ExampleTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.072 s -- in ExampleTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  6.819 s
[INFO] Finished at: 2024-10-02T13:32:27Z
[INFO] ------------------------------------------------------------------------
Instrutor Cristian

Por que 'aparentemente'?

Em TDD, temos que supor que um projeto recém-criado só vai ser considerado testado se tanto a framework de testes quanto os nossos códigos irão falhar quando deveriam falhar ou passar nos testes quando deveriam passar. Ferramentas e logs podem estar mal configurados, gerando falsos positivos/negativos, o que pode nos enganar.

Instrutor Cristian

Vamos aos primeiros testes. Por onde começar?

No Design Orientado a Objeto, todo código deve estar contido numa classe. Logo, um código de teste é abrigado pelas chamadas Classes de Teste. Uma classe de teste precisa definir métodos que vão ser executados e nestes vão constar nossas linhas de código de teste.

Vamos testar 2 coisas importantes:

  • Se a framework de teste encontra nossa classe de teste;
  • E se pelo menos nosso método de teste foi encontrado.

Para facilitar, temos uma classe de teste pré-pronta chamada ExampleTest.java na pasta test/java.

Instrutor Cristian
Instrutor Cristian

Vamos analisar com cuidado...

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class ExampleTest {
    @Test
    void nothing() {
        // Dummy Method
        Assertions.assertTrue(true);
    }
}
Instrutor Cristian
  • Todo nome de classe de teste no JUnit precisa conter a palavra Test como em ExampleTest
  • Uma classe de teste consiste em definir métodos que vão executar as rotinas de teste. A visibilidade do método de teste pode ser default, public ou protected; jamais private. Se for private, o JUnit não consegue encontrar para executar;
  • @Test é uma anotação do JUnit para indicar que em seguida haverá um método de teste. Precisamos importar do pacote org.junit.jupiter.api.
  • void nothing() {...} chamamos de Dummy Method (serve como ponto de partida para um novo teste e quando estamos indecisos com nome do método de teste).
Instrutor Cristian
Instrutor Cristian

O comando mvn clean test dispara a execução de todos os testes. Verifique se consta a informação de que a classe de teste ExampleTest foi encontrada e pelo menos 1 elemento de teste foi executado (trecho Tests run: 1).

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running ExampleTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.072 s -- in ExampleTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Instrutor Cristian

E agora? Podemos montar uma Lista de Casos de Teste

Consiste em definir o que vamos testar da nossa aplicação

É o conselho do Martin Fowler em Test Driven Development (2023).

Instrutor Cristian
Instrutor Cristian

Escrevendo nossa lista de casos de teste

Devemos separar a lista em 2 categorias:

  1. Grupo ou Caso de teste mais importante: implementa a funcionalidade mais esperada;
  2. Grupo ou Caso de teste degenerado ou de suporte: implementa funcionalidades ditas secundárias (Bob).
Instrutor Cristian

Caso de Teste mais importante

  • Push de X e Y, quando fizermos pop sairá Y e outro pop fará sair X
stack.push(99);
stack.push(88);
var X = stack.pop(); // sai 88
var Y = stack.pop(); // sai 99
Instrutor Cristian

Grupo de Testes Degenerados/Suporte

A pilha está cercada de propriedades e operações que dão apoio à regra principal. Podemos listar as constatações primordiais:

  • Ao ser criada, a pilha precisa estar vazia;
  • Push de um elemento faz a pilha deixar de estar vazia;
  • Push e Pop faz a pilha estar vazia;
  • Pop de uma pilha vazia faz ela disparar um erro dizendo que está vazia;
  • Push de 2 elementos faz o tamanho corresponder a essa quantidade;
  • Push de um elemento X, quando fizermos pop, este mesmo X será retornado.
Instrutor Cristian

Por onde começar?

Bob aconselha iniciar pelos casos degenerados. A razão por trás disso será discutida futuramente.

O primeiro caso degenerado pode nos ajudar verificar se também a framework de teste passa quando deveria passar ou falha quando deveria falhar.

Instrutor Cristian

1º Caso de Teste: ao ser criada, a pilha precisa estar vazia.

  • Renomeie a classe ExampleTest para StackTest. Certifique-se de que o arquivo também precisa ser renomeado para StackTest.

  • Insira var stack = new Stack() como no código a seguir

Instrutor Cristian
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class StackTest {
    @Test
    void nothing() {
        var stack = new Stack();
        Assertions.assertTrue(true);
    }
}
Instrutor Cristian
Instrutor Cristian

Rodando os testes no Codespaces

  • Pela linha de comando, chame os testes executando
cd projeto-java-tdd # nao se esqueca de estar dentro da pasta do projeto
mvn clean test
  • Caso tenha a extensão Extension Pack for Java habilitada, terá uma opção aparecendo como
    alt text
Instrutor Cristian
Instrutor Cristian

Vamos rodar os testes clicando em

alt text

Veremos uma falha de compilação. Dizer "Rode os testes" envolve 3 etapas:

  1. Compilar o código
  2. Executar os métodos de teste contidos nas classes de teste (classes com nome sufixado em Test, como em StackTest)
  3. Gerar relatório de testes
Instrutor Cristian

Ao compilar, teremos a falha notificada

alt

Instrutor Cristian

Cumprindo a Etapa 1: Escreva uma linha de código de teste que não vai compilar e a 1ª REGRA: Escreva um teste que vai falhar por falta de código de produção.

Instrutor Cristian

Vamos criar a classe Stack.

  • Crie um arquivo chamado Stack.java na pasta src/main/java.
Instrutor Cristian

alt text

Instrutor Cristian

Rode os testes

alt text

Observe que tudo roda com sucesso!

alt

Instrutor Cristian
public class Stack {}

Ao compilarmos, verificamos a resolução do problema de compilação.

Cumprimos a Etapa 2: Escreva uma linha de código de produção que faz o código de teste compilar.

Instrutor Cristian

Cumprimos também 2 regras simultaneamente

2ª REGRA: Ao ver a falha de lógica ou de compilação acontecendo, resolva ambas escrevendo código de produção.

3ª REGRA: Quando o teste falhar, escreva o código que o faz passar, mas que seja suficiente e não mais do que isso.

TDD é algo atômico. Não devemos ficar implementando diversas linhas com a pressa de se concluir a implementação. A ordem são movimentos do tipo baby steps.

Instrutor Cristian

O conceito de Asserção: uma medida verificadora

Para prosseguirmos, precisamos realizar uma asserção sobre o comportamento esperado. Asserção significa "afirmar, enunciar, alegar". Numa asserção, impomos um significado lógico que esperamos que o código de produção cumpra após ser executado.

Vamos precisar fazer uma afirmação sobre o estado interno esperado que a pilha precisa alcançar ao ser criada. Dizemos "afirma-se que um comportamento esperado foi executado e verificado".

Instrutor Cristian

Fazendo a asserção correta

Vamos sair disso

Assertions.assertTrue(true);

para isso

Assertions.assertTrue(stack.isEmpty());
Instrutor Cristian

A classe StackTest ficará assim

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class StackTest {
    @Test
    void nothing() {
        var stack = new Stack();
        Assertions.assertTrue(stack.isEmpty());
    }
}
Instrutor Cristian

Cumprimos a Etapa 3: Escreva uma nova linha de código de teste que não compila.

Teremos uma falha de compilação.

PROBLEMA: O método isEmpty() não foi criado na classe Stack.

Instrutor Cristian

Solução:

Criar o método isEmpty() na classe Stack.

Instrutor Cristian

Entretanto, temos que definir qual valor de retorno de isEmpty(). true ou false?

public class Stack {
    public boolean isEmpty() {
        return ???;
    }
}
Instrutor Cristian

Resolvendo os problemas de compilação

cumprimos a Etapa 4: Escreva uma nova linha de código de produção que faz o teste compilar;

Instrutor Cristian

Respeitemos a etapa 5

Etapa 5: Escreva 1 ou 2 linhas de código de teste que compila mas falha na asserção.

public class Stack {
    public boolean isEmpty() {
        return false;
    }
}

Assim cumprimos a etapa adequadamente. Por outro lado, temos falha nos testes por descumprir o requisito lógico da asserção.

Instrutor Cristian

alt text

Instrutor Cristian

Fazendo o código de teste passar

A implementação mais simples que faz passar é a seguinte

public class Stack {
    public boolean isEmpty() {
        return true;
    }
}

Cumprimos a Etapa 6: Escreva 1 ou 2 linhas de código de produção que passa na asserção. Cumprimos a Regra 2 e 3.

Instrutor Cristian
Instrutor Cristian

Cumprimos com sucesso o nosso primeiro caso de teste. Recapitulando: ao ser criada, a pilha precisa estar vazia.

Sumário deste exercício:

  • Implementamos uma classe de teste e o código de produção que vai atendê-la;
  • Verificamos que nossa classe de teste é encontrada pela framework de teste e o método assinalado como @Test o faz ser executado. Constatamos que foi encontrado e executado com sucesso;
  • Verificamos que o teste passa quando deveria passar e falha quando deveria falhar.
Instrutor Cristian

Calma lá... E a Regra 4?

Limpando a sujeira 🧹: acha que vamos parar por aqui com um código que pode ser simplificado!?

Temos um porto seguro. O código de teste passou. Devemos agora limpá-lo. Tanto o código de teste quanto o de produção precisam ser limpos sempre. Renomeie métodos e variáveis para melhorar a legibilidade assim que possível.

Instrutor Cristian

Primeiro ato de limpeza 🧹: faça a importação estática de assertTrue

import org.junit.jupiter.api.Test;  
  
import static org.junit.jupiter.api.Assertions.assertTrue;  
  
public class StackTest {  
    @Test  
    void nothing() {  
        var stack = new Stack();  
        assertTrue(stack.isEmpty());  
    }
}
Instrutor Cristian

Segundo ato de limpeza 🧹: Renomeie o método de teste para corresponder melhor ao que se está fazendo. Aqui somos livres para definir. Existem algumas opções populares de nomeação:

  • shouldStackIsEmpty: a pilha deve estar vazia (omitem-se palavras de ligação para simplificar a escrita e leitura);
  • givenEmptyStack_whenCreate_thenIsEmpty: alternativa popular chamada BDD low-level;
  • canCreateEmptyStack: forma que iremos adotar.

Muitas vezes surgem dúvidas de nomeação. Com a prática é possível notar um certo padrão. Existem casos complexos que será inevitável termos nomes de métodos bem longos. É preciso bom senso.

Instrutor Cristian

Aqui entra o Clean Code?

Recapitulando os princípios do Clean Code (autores Bob e Ron Jeffries):

  • Todos os testes passam;
  • Não existe duplicação;
  • Menor quantidade possível de classes, métodos, funções e etc.
    • Quanto mais código, mais custosa é a sua alteração;
    • Não deve existir código em desuso. Deve ser apagado.

Um erro comum é ainda ficar fazendo transformações no código sem ter testes.
De acordo com Bob: sem testes, sem Clean Code. Uma transformação sem teste pode gerar efeitos colaterais imprevisíveis. (famosos "debug das 3h da manhã")

Instrutor Cristian

Implementando os próximos casos de teste

  • Ao ser criada, a pilha precisa estar vazia;
  • Push e Pop faz a pilha estar vazia
  • Pop de uma pilha vazia faz ela disparar um erro dizendo que está vazia
  • Push de 2 elementos faz o tamanho corresponder a essa quantidade
  • Push de um elemento X, quando fizermos pop, este mesmo X será retornado
  • Push de X e Y, quando fizermos pop sairá Y e outro pop fará sair X (regra principal)

Por brevidade, a prática deve ser continuada com o instrutor.

A cada teste terminado, devemos riscar da nossa lista de casos de teste.

Instrutor Cristian
  • Ao ser criada, a pilha precisa estar vazia;
  • Push e Pop faz a pilha estar vazia
  • Pop de uma pilha vazia faz ela disparar um erro dizendo que está vazia
  • Push de 2 elementos faz o tamanho corresponder a essa quantidade
  • Push de um elemento X, quando fizermos pop, este mesmo X será retornado
  • Push de X e Y, quando fizermos pop sairá Y e outro pop fará sair X (regra principal)
Instrutor Cristian
  • Ao ser criada, a pilha precisa estar vazia;
  • Push e Pop faz a pilha estar vazia
  • Pop de uma pilha vazia faz ela disparar um erro dizendo que está vazia
  • Push de 2 elementos faz o tamanho corresponder a essa quantidade
  • Push de um elemento X, quando fizermos pop, este mesmo X será retornado
  • Push de X e Y, quando fizermos pop sairá Y e outro pop fará sair X (regra principal)
Instrutor Cristian
  • Ao ser criada, a pilha precisa estar vazia;
  • Push e Pop faz a pilha estar vazia
  • Pop de uma pilha vazia faz ela disparar um erro dizendo que está vazia
  • Push de 2 elementos faz o tamanho corresponder a essa quantidade
  • Push de um elemento X, quando fizermos pop, este mesmo X será retornado
  • Push de X e Y, quando fizermos pop sairá Y e outro pop fará sair X (regra principal)
Instrutor Cristian
  • Ao ser criada, a pilha precisa estar vazia;
  • Push e Pop faz a pilha estar vazia
  • Pop de uma pilha vazia faz ela disparar um erro dizendo que está vazia
  • Push de 2 elementos faz o tamanho corresponder a essa quantidade
  • Push de um elemento X, quando fizermos pop, este mesmo X será retornado
  • Push de X e Y, quando fizermos pop sairá Y e outro pop fará sair X (regra principal)
Instrutor Cristian
  • Ao ser criada, a pilha precisa estar vazia;
  • Push e Pop faz a pilha estar vazia
  • Pop de uma pilha vazia faz ela disparar um erro dizendo que está vazia
  • Push de 2 elementos faz o tamanho corresponder a essa quantidade
  • Push de um elemento X, quando fizermos pop, este mesmo X será retornado
  • Push de X e Y, quando fizermos pop sairá Y e outro pop fará sair X (regra principal)
Instrutor Cristian

Como seria utilizar nossa Pilha?

  1. Use o código criando a classe principal com método main.
  2. Com jshell, podemos brincar carregando pacotes compilados
  • Caso opte pelo jshell, vamos precisar inserir a classe Stack e StackTest dentro de um pacote como org.example.
jshell --class-path target/project-java-tdd-1.0-SNAPSHOT.jar
> import org.example.Stack
> var stack = new Stack()
> stack.push(99)
> stack.push(88)
> stack.pop()
> stack.getSize()
> stack.pop()
> stack.pop()
Instrutor Cristian

Certo. Temos um TAD de Pilha básico. Qual a vantagem de se ter feito por TDD?

A ideia principal do software é sua capacidade de ser modificado mais facilmente do que o hardware. No entanto, como iremos lidar com as incertezas das mudanças? Uma pequena alteração pode ser catastrófica.

A resposta para essa pergunta é a cobertura de teste. A cobertura de teste nos ajuda a dimensionar o que foi testado e possíveis lacunas. A cobertura é dada por um percentual do código calculado através da quantidade de linhas exploradas pelos testes sobre o total de linhas a serem cobertas.

Instrutor Cristian

Podemos expressar como

  • = percentual de cobertura de teste
  • = quantidade de linhas exploradas pelos testes
  • = quantidade total de linhas a serem cobertas

Os softwares de análise de cobertura executam algoritmos de Complexidade Ciclomática para contar os trechos de código que foram referenciados nos testes. Uma explicação detalhada se encontra em Coverage Counters.

Instrutor Cristian

Qual valor de nos dá a garantia de mudanças com menos impacto possível?

Instrutor Cristian

É o mesmo que dizer que

Isto é, todas as linhas de código precisam ser cobertas com testes. Devemos lutar para evitar exceções. Exceções criam lacunas potenciais.

Instrutor Cristian

E qual é a nossa cobertura de teste? Precisaremos adicionar o JaCoCo para responder.

Instrutor Cristian
Instrutor Cristian

Adicionando o plugin do JaCoCo (Java Code Coverage)

Emmet: build>plugins>plugin:jacoco
Live Template: plugin:jacoco

Instrutor Cristian

Executando todos os testes e Gerando Relatório de Cobertura de Teste

mvn clean verify

Etapas que serão executadas em sequência:

  • clean: apaga tudo da pasta target;
  • verify: executa todos os testes e gera relatório de cobertura de testes do JaCoCo
Instrutor Cristian

Abrir report do JaCoCo

  • No macOS:
open target/site/jacoco/index.html
  • Pode abrir em qualquer browser sem problemas.
Instrutor Cristian

Verificamos que foi atingido 100% de cobertura

Alcançar 100% é o ideal a ser buscado. .

Instrutor Cristian

Outra grande vantagem do TDD: código desacoplado

Benefícios gerados de um código desacoplado e testado:

  • Pronto para ser implantado em produção;
  • Preparado para receber mudanças;
  • Efeitos colaterais das mudanças podem ser controladas e revertidas a qualquer momento;
  • Mais fácil de identificar bugs devido ao baixo acoplamento.
Instrutor Cristian

Ponto de preocupação

devido

Quer dizer, menos de 100%.

Não esquente muito, mas tenha atenção

Estar entre 90% e 99.999...% pode ser tolerável em algumas circunstâncias específicas, como em partes mais complexas de se testar (artefatos de configuração). Devemos justificar a razão para tal.

Instrutor Cristian

"Testamos no app e funciona. Mas não teste muito senão estraga e ninguém quer bugfix na sexta! Hotfix talvez na semana que vem!"

é o mesmo que

Riscos:

  • Pequenas alterações levando a bugs gigantescos;
  • O que funcionava deixou de funcionar;
  • Não sabemos o que o software realmente faz;
  • Alguém sempre vai ter que testar manualmente (QA dedicado desenvolvendo LER);
  • Clientes irritados com a demora das correções.
Instrutor Cristian

Fenômeno da Generalidade e Especificidade

"À medida que os testes ficam mais específicos, o código fica mais genérico." (Uncle Bob em Clean Craftsmanship).

Quanto mais testes refinados colocamos, isto é, quanto mais específicos forem, mais geral precisa ser o código de produção.

Instrutor Cristian

Desse fenômeno, surgem transformações

Mais tarde Bob apresenta em seu livro a ideia do TPP = Premissa Prioritária de Transformação

Instrutor Cristian

Exemplos de transformações

Instrutor Cristian

Um valor constante se transforma em variável

  • Uma variável é a forma geral de valores constantes
  • Costuma ser a transformação que aprendemos sem saber que existe essa teoria formalizada
Instrutor Cristian

Um while é um if generalizado

int f(int n) {
    int i = 1, r = -1;

    if (n < 1) return r;

    r = g(n, i);
    i++;

    if (n == i) return r;

    r = g(n, i);
    i++;

    if (n == i) return r;

    r = g(n, i);
    i++;

    if (n == i) return r;

    return r;
}

Instrutor Cristian

É o mesmo que

int f(int n) {
    int i = 1;
    int r = -1;

    while (n >= i) {
        r = g(n, i);
        i++;
    }

    return r;
}
Instrutor Cristian

Um if é um while degenerado (simples)

Podemos desfazer um while numa sequência de ifs. Só fazer o processo inverso do exemplo anterior.

Instrutor Cristian

Generalize sempre que possível

Na página 59 (Clean Craftsmanship), Bob aponta a Regra 5: generalize sempre que possível.

Quanto mais adiarmos a generalização, mais difícil será atender os testes e implementar um código genérico o suficiente.

Instrutor Cristian

Sobre Loops, Edmonds tem uma definição geral interessante

Edmonds apresenta algumas generalizações de algoritmos em "How to Think about Algorithms" (2008).

Instrutor Cristian

Edmond estabelece que todo código iterativo é definido como

alt text

Instrutor Cristian

Podemos resumir em Java como

// Zona de pre-loop
int i = 0;

// Loop
while (true) {
    // Zona de verificacao de invariante de loop
    // Condicao de saida do loop
    if  (true) break;

    // Codigo do loop
    i++;
}

// Zona do pos loop
Instrutor Cristian

O que é Invariante de Loop?

É a verdade que se mantém a cada iteração de um loop.
Se antes do código do loop o invariante for falso, o código está incorreto.

Edmonds ressalta que devemos ver um algoritmo como uma sequência de asserções (afirmações) que se mantém verdadeiras ao longo da execução.

Este é o Santo Graal para saber se um código de loop está correto?!

No exemplo do FindMax, veremos que isso por si só não é o suficiente.

Instrutor Cristian

Implementando o Algoritmo de Fatorial

Versões Iterativa e Recursiva

  • Vamos criar uma classe chamada FactTest
  • Método de teste canFact

Vamos também identificar o Invariante de Loop

Para entender melhor este exemplo, mais à frente veremos as Leis do Anamorfismo e Catamorfismo do Cálculo de Programas.

Instrutor Cristian

Existem outras abordagens além do TDD?

SIM! Para Uncle Bob:

Debugging (uso do GNU Debugger ou usando loggers)

  • O mesmo não incentiva ao se programar profissionalmente. Não tem problema usar de vez em quando como um tira-dúvidas.
Instrutor Cristian

Métodos formais

  • Uso da Álgebra e lógica
  • Uso de Autômatos Finitos Determinísticos
Instrutor Cristian

Para Tesson et al. (2011): a programação apresenta 2 grandes abordagens em geral

  1. CONSTRUIR O PROGRAMA E VERIFICAR SE ATENDE OS REQUISITOS DEPOIS DE PRONTO:
    • Pode se tornar muito difícil para grandes programas
    • Muitos programadores acabam esquecendo
  2. CONSTRUIR O PROGRAMA E SUA PROVA DE CORREÇÃO SIMULTANEAMENTE:
    • Dispensa a prova do alcance dos requisitos depois de pronto
    • Uso de Métodos Formais (matemática e lógica): conseguem provar que um programa está correto ou não (se tiver muita paciência)
Instrutor Cristian

Uma percepção da prática no dia-a-dia

O uso de métodos formais consegue uma generalidade e correção muito precisa. Entretanto, para o trabalho diário, métodos formais são desafiadores

  • Podem ser úteis para aplicações altamente críticas
  • Exigem mais habilidades de matemática e lógica
  • Dependendo do problema, podem ser muito complexos
Instrutor Cristian

Sobre a abordagem dos ciclos de teste e implementação em pequenos passos

Esta alternativa combina características:

  • Atua como um debug mais suave
  • Funciona como uma documentação
  • Consegue atuar como prova de algoritmo mais prática para o dia-a-dia do que os métodos formais
  • Facilita aplicações de técnicas de construção e reescrita
  • Fundamenta-se em disciplina Agile: Extreme Programming (XP)
Instrutor Cristian

Aspecto importante do aprendizado de programação com testes: noção de causa-efeito

Instrutor Cristian

O aprendizado de programação baseado em testes e análise de causa-efeito

  • Podemos aprender a programar com pequenos trechos com adição de exemplos mais complexos (Ramalho, 2022);
  • A repetição ajuda no automatismo de raciocínio de causa-efeito.

A rua está molhada porque choveu.
- Causa: A chuva
- Efeito: Fez a rua ficar molhada

Instrutor Cristian

Em programação, é mais comum pensar "invertido"

  • Invertido: por que temos que inventariar causas implementáveis
    que originam os efeitos desejados
  • Um cadastro de produto precisa estar feito (Efeito desejado)
  • O que pode habilitar o cadastro? (Investigação das possíveis causas implementáveis)
    • Requisitar um gerente de estoque
    • Escrever uma ficha de solicitação e deixar na mesa do gerente
    • Enviar um SMS para o estoquista
    • Um gerente e estoquista solicitam no início do dia
    • ...o que a criatividade permitir
Instrutor Cristian

Exemplo com Programação Web

  • O servidor retornou a mensagem "Produto cadastrado com sucesso!" (Efeito)
    Como se dá? Através de uma resposta HTTP com estado "200 OK"
  • Causa implementável: um usuário requisitar o seu cadastro
    Como? Através de uma requisição HTTP do tipo "POST"

Devemos descobrir o "Como" fazer dar certo.

Instrutor Cristian

Entretanto, existem variações do "Como" e alguns problemas

  • Causa mais provável: um usuário requisitou o seu cadastro
    • Causas implementáveis
      • Através de uma requisição HTTP do tipo "POST"
      • Através de uma requisição TCP na porta 587 sob protocolo SMTP (envio de email)
      • Através de Web Sockets
Instrutor Cristian
  • Com o passar do tempo, uma causa implementável pode se tornar:
    • custosa (uso de servidores de dados caros)
    • irrelevante (como envio de e-mail em muitos casos)
    • pouco confiável (cadastro sob HTTP sem criptografia)
    • pouco reproduzível/manual (escrever um cartão postal)
Instrutor Cristian

Sobre Métodos Formais

Alguns métodos formais podem ajudar a simplificar determinados tipos de algoritmos

Existe uma área de estudo específica: Cálculo de Programas

Grande uso em linguagens funcionais (Haskell, Clojure, ...) e em algumas com suporte a técnicas, como Java e Kotlin

Instrutor Cristian

As duas leis formais: Anamorfismo e Catamorfismo

Instrutor Cristian

Lei do Anamorfismo (ana = produzir)

  • pode ser uma Lista, uma Árvore ou algum representante de coleção
  • é uma função geradora que recebe um número natural como entrada ()

Lei do Catamorfismo (cata = destruir)

  • De podemos extrair um número natural
  • é uma função redutora (reduce) que recebe uma coleção como entrada
Instrutor Cristian

Hands-on: Construindo Fatorial Anamórfico e Catamórfico

Instrutor Cristian

O que está acontecendo com o Invariante de Loop?

int r = n;
int i = 1;

var list = new ArrayList<Integer>();

while (true) {
    // Invariante de Loop
    list.add(n - i + 1);
    assertEquals(list.stream().reduce((a, b) -> a * b).get(), r);

    if (i == n) break;

    r = r * (n - i);

    i++;
}
Instrutor Cristian
n = 2
    iteracao (i = 1)
    t = n - i + 1 = 2 - 1 + 1 = 2
    L = [t] = [2]
    r = L[0] = 2

    iteracao (i = 2)
    t = n - i + 1 = 2 - 2 + 1 = 1
    L = [2, t] = [2, 1]
    r = L[0] * L[1] = 2
Instrutor Cristian
n = 3
    iteracao (i = 1)
    t = n - i + 1 = 3 - 1 + 1 = 3
    L = [t] = [3]
    r = L[0] = 3

    iteracao (i = 2)
    t = n - i + 1 = 3 - 2 + 1 = 2
    L = [3, t] = [3, 2]
    r = L[0] * L[1] = 6

    iteracao (i = 3)
    t = n - i + 1 = 3 - 3 + 1 = 1
    L = [3, 2, t] = [3, 2, 1]
    r = L[0] * L[1] * L[2] = 6
Instrutor Cristian

Vamos para a Unidade II

Clique no link acima.

Instrutor Cristian

Exercícios

Instrutor Cristian

1. Implemente o Algoritmo de Fibonacci

  • Versões iterativa e recursiva
Instrutor Cristian

2. Implemente FindMax

  • Versão iterativa
    • Cuidado com generalizações sem identificar valores de entrada suficientes.
      Nem mesmo Invariantes de Loop podem apontar a corretude...
      assertEquals(4, findMax(List.of(1, 2, 3, 3, 4)));
  • Usando a Lei do Catamorfismo
  • Identifique o Invariante de Loop
Instrutor Cristian
private int findMax(List<Integer> list) {
    if (list.isEmpty()) return -1;
    else if (list.size() == 1) return 0;

    int i = 0, j = 0;

    System.out.println("Analise de asserções de invariante de loop");

    while (true) {
        int nextIndex = i + 1;
        // Invariante de Loop: L[j] == max(L[0...i+1])
        System.out.println(list.get(j) + " é o max de " + list.subList(0, nextIndex));
        assertEquals(list.get(j), max(list.subList(0, nextIndex)));

        if (i == list.size() - 1) break;

        if (list.get(i) < list.get(nextIndex)) {
            j = nextIndex;
        }

        i++;
    }

    System.out.println("-------");

    return j;
}
Instrutor Cristian