DEV Community

Cover image for Testes 101 - Testando aplicações Java
Victor Osório
Victor Osório

Posted on • Edited on

Testes 101 - Testando aplicações Java

Testes são fundamentais. Se você deseja ser desenvolvedor Java lhe recomendo pelo menos conhecer o que são TDD, Maven e JUnit. E vou te apresentar o porque.

TDD

TDD significa Test-Driven-Development. Se você imagina que é criar o testes e depois o código, você está um pouco enganado. TDD é uma disciplina um pouco diferente.

TDD é uma disciplina, não é algo que pode ser explicado. É uma cultura que você tem que aprender, e treinar. Depois de anos você vai ver já melhorou bastante, mas tem muito mais a aprender.

Ciclos do TDD

Pra explicar o TDD facilmente, temos que falar dos ciclos. Nem todos os testes criados serão usados e eles não devem refletir o requisito final. Você tem que usar Baby Steps, passos de bebê, em cara ciclo. Imagine um ciclo como um rodada de desenvolvimento de 10 a 30 minutos:

  • [R] Você cria um Test, ele deve falhar.
  • [G] Você implementa o código para o teste funcionar
  • [R] Você Refatora o código

Ciclo RGR

Ao final de um tempo de desenvolvimento você terá vários testes. Alguns podem ser descartados, outros vão ficar. Eu recomendo ficar apenas os que representem a funcionalidades do código. E recomendo também que cubram o máximo possível o seu código.

Maven

Se estamos falando de testes, estamos falando de Processo de Build. Se você cria os seus testes e não os colocar para serem executados automaticamente, você não fez praticamente nada.

O Maven abstrai cada build criando um ciclo com fases. Então os testes sempre serão executados se você deseja executar ou empacotar o seu projeto.

Para criar um projeto usando Maven, instale o Maven e execute:

mvn archetype:generate -DgroupId=io.vepo.tests -DartifactId=testsExample -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=true
Enter fullscreen mode Exit fullscreen mode

Você vai observar duas coisas:

  • o Maven criou um arquivo pom.xml colocando como dependência o JUnit, e que versão antiga! 🙄
  • o Maven criou duas pastas de código: src/main/java e src/test/java.
<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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.vepo.tests</groupId>
  <artifactId>testsExample</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>testsExample</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
Enter fullscreen mode Exit fullscreen mode

Estrutura de diretório

Mas onde o Maven configura que os testes devem ser executados durante a build ou como ele descobre os testes? O Maven se baseia na ideia convention-over-configuration, ou seja, Convenção acima de Configuração. Para realizar certas configurações basta apenas usar a convenção apropriada.

O projeto criado já vem com alguns testes JUnit configurado, porém é usado a versão 3.8.1... Alguém por favor atualiza o github.com/apache/maven-archetypes, por favor!

JUnit 5

O JUnit é o framework que irá gerenciar o ciclo de vida de seus testes. Cara classe dentro de src/test/java contendo um método com a annotation org.junit.jupiter.api.Test será executado como testes. Assim para migrarmos o arquivo gerado automaticamente, basta mudar o seguinte conteúdo:

<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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.vepo.tests</groupId>
  <artifactId>testsExample</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>testsExample</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>5.5.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
        <!-- Need at least 2.22.0 to support JUnit 5 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.0.0-M3</version>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>
</project>
Enter fullscreen mode Exit fullscreen mode
package io.vepo.tests;

import static org.junit.jupiter.api.Assertions.assertEquals;

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

public class AppTest 
{   
    @Test
    @DisplayName("Test if it works")
    public void simpleTest() {
        assertEquals("OK", "OK");
    }
}
Enter fullscreen mode Exit fullscreen mode

Observe que com o JUnit 5 eu consigo dar nomes aos testes, isso facilita em muito a identificação de um erro. Eu costumo usar frases que definem as features testadas. Isso facilita quando preciso fazer manutenção em código escrito meses, ou anos, antes.

Ciclo de Vida

O ciclo de vida de um teste passa pela execução métodos antes/depois da classe ser criada e métodos antes/depois da execução de cada teste. Dê uma olhada na documentação. Há vários exemplos de como usar @BeforeAll, @BeforeEach, @AfterEach e @AfterAll.

Se eu for detalhar cada feature do JUnit 5, esse post não terá fim. São muitas, conheça elas, assim você pode construir bons testes.

Asserções

O mais importante do JUnit não é apenas a execução dos testes, mas a validação dos resultados.

O JUnit provê uma classe com métodos estáticos para realizar isso. Na documentação oficial há vários exemplos.

@Test
void standardAssertions() {
    assertEquals(2, calculator.add(1, 1));
    assertEquals(4, calculator.multiply(2, 2),
            "The optional failure message is now the last parameter");
    assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
            + "to avoid constructing complex messages unnecessarily.");
}
Enter fullscreen mode Exit fullscreen mode

Tests DSL

DSL significa Domain-Specific Language. Você cria uma DSL seu código pode ser lido como uma linguagem. Em testes é comum se construir uma DSL usando os termos Given-When-Then. Você pode fazer isso em português Dado-Quando-Então:

  • (Dado) Qual o contexto que o teste é executado?
  • (Quando) Qual ação vai ser testada?
  • (Então) O que deve ser validado?

Um bom exemplo pode ser:

dadoNovoUsuário()
    .executaChecking()
        .validaReserva();
Enter fullscreen mode Exit fullscreen mode

Aí fica de você implementar cada método e reutilizar ele quando possível.

AssertJ

Há algumas bibliotecas que auxiliam na construção dessa DSL. Eu gosto muito da AssertJ. Com ela é possível usar uma DSL para validação de resultados complexos.

// extracting multiple values at once grouped in tuples
assertThat(fellowshipOfTheRing).extracting("name", "age", "race.name")
                               .contains(tuple("Boromir", 37, "Man"),
                                         tuple("Sam", 38, "Hobbit"),
                                         tuple("Legolas", 1000, "Elf"));
Enter fullscreen mode Exit fullscreen mode

Mocks

Mock significa Imitação. Imagina no caso de estarmos acessando uma base de dados. Você tem duas opções, ou liga a base de dados e testa diretamente nela, ou você mocka o acesso a base. O problema da primeira abordagem é que ela torna o teste mais abrangente. Não estaremos fazendo um Teste Unitário, mas um Teste de Integração.

Não há problema em fazer Testes de Integração, mas eles serão muito mais lentos. Outro problema é que muitas vezes você não precisa testar uma base de dados. Mas as vezes é bom testar a integração, sempre evita um NullPointerException! ☠️

Há algumas boas bibliotecas para Mock, vou falar um pouco do Mockito e PowerMock

Mockito

Mockito serve para criar classes onde o código original pode ou não ser executado.

Quando você quer executar o código original, estamos falando de um spy. O código é executado e você pode validar o que foi feito.

Um Spy pode ser criado usando Mockito.spy ou usando o Jupiter Extension para o JUnit 5.

@Test
@DisplayName("Testa o exemplo de Mock para UserRepository")
public void addUserInMemoryTest() {

    User userWithoutId = new User();
    userWithoutId.setEmail("vepo@vepo.com");
    userWithoutId.setUsername("vepo");

    assertThat(userRepositoryInMemory.add(userWithoutId)).hasFieldOrPropertyWithValue("id", 1L);

    verify(userRepositoryInMemory).add(userWithoutId);
}
Enter fullscreen mode Exit fullscreen mode

Observe que um Spy serve para verificar se o método foi chamado com um valor especifico. Ou seja, precisamos ter uma implementação concreta da classe.

Agora quando falamos de Mocks não precisamos de implementações concretas. Podemos usar em interfaces. Quando é criado um mock, o código real não é chamado, são sempre retornados valores vazios ou nulls. Então é preciso definir o que será retornado e quando.

@Test
@DisplayName("Testa o exemplo de Mock para UserRepository")
public void addUserTest() {
    User userWithoutId = new User();
    userWithoutId.setEmail("vepo@vepo.com");
    userWithoutId.setUsername("vepo");

    User userWithId = new User();
    userWithId.setId(1L);
    userWithId.setEmail("vepo@vepo.com");
    userWithId.setUsername("vepo");

    when(userRepository.add(userWithoutId)).thenReturn(userWithId);

    assertThat(userRepository.add(userWithoutId)).hasFieldOrPropertyWithValue("id", 1L);
}
Enter fullscreen mode Exit fullscreen mode

Cobertura de Testes

As coisas não andam se não tivermos estatísticas! Então para testes, precisamos saber com exatidão qual é a cobertura de testes do nosso projeto. Se não colocarmos isso em prática, muito rapidamente a cobertura irá cair e nem perceberemos.

Uma ferramenta para gerar um relatório de cobertura é o JaCoCo

Para configurar o JaCoCo no Maven, basta adicionar ele como um plugin e configurar quando será executado.

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.1</version>
    <executions>
        <execution>
            <id>prepare-agent</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>
Enter fullscreen mode Exit fullscreen mode

Depois para executar:

mvn clean test jacoco:report
Enter fullscreen mode Exit fullscreen mode

Relatório de Tests

No caso será gerado um relatório. Esse relatório pode ser armazenado no Jenkins ou mesmo usado pela próxima ferramenta que vamos ver.

Code Smells

Antes de partir para última ferramenta, vamos definir uma coisa.

Code Smells são construções que podem trazer má qualidade ao código. Eliminando eles, você pode melhorar a qualidade do seu código.

Análise Estática de Código

Como última ferramenta, vamos falar de Análise Estática de Código. Imagina se você pudesse analisar o seu código e encontrar bugs ou Code Smells. Seria bom, não?

Mas temos isso e de graça. Você pode usar o SonarQube. Com esse plugin para o Maven você pode criar um servidor para armazenar a qualidade atual do seu código e construir uma timeline dele. Assim você pode desafiar o time a reduzir o número de Code Smells em 50%. Ou em aumentar a covertura de testes até um determinado patamar.

Para integrar no Maven, basta colocar:

<plugin>
    <groupId>org.sonarsource.scanner.maven</groupId>
    <artifactId>sonar-maven-plugin</artifactId>
    <version>3.7.0.1746</version>
    <executions>
        <execution>
            <phase>verify</phase>
            <goals>
                <goal>sonar</goal>
            </goals>
        </execution>
    </executions>
</plugin>
Enter fullscreen mode Exit fullscreen mode

Ao adicionar o plugin, crie o projeto no Github e já inicialize ele no SonarCloud. Ao iniciar um projeto, você pode pegar um Token que deve ser usado na build.

mvn -Dsonar.login=<SONAR_TOKEN> verify sonar:sonar
Enter fullscreen mode Exit fullscreen mode

Caso queira rodar o Sonar em projetos internos da sua empresa, você só precisa de um servidor e uma instalação do Sonar. É simples de configurar.

Tudo Junto

Agora vamos responder a última pergunta. Quem vai rodar tudo isso? Você pode configurar algumas ferramentas. Entre elas podemos citar:

Ferramenta Contexto
Jenkins Para rodar projetos internos. Não é um serviço cloud.
Github Actions Excelentes para projetos Github.
TravisCI Could e facilmente integrado com o Github.

Conclusão

Testes são fundamentais e você não precisa saber fazer antes de começar a desenvolver. MAS... Se você souber criar testes, seu código terá muita estabilidade.

Treine fazer testes. Treine TDD. Conheça as ferramentas e boa sorte!

Alt Text

Top comments (0)