Teste de unidade Java com JUnit 5: melhores práticas e técnicas explicadas

Teste de unidade Java com JUnit 5: melhores práticas e técnicas explicadas

Domine o teste de unidade Java: mergulhe nas ferramentas, práticas recomendadas e técnicas para garantir um código robusto. Aumente a confiabilidade do software e entregue perfeitamente!

Imagem em destaque

Procurando impulsionar seus esforços de desenvolvimento Java? Este guia explora o mundo dos testes Java, abordando conceitos básicos e técnicas avançadas. Você aprenderá sobre a importância do Test Driven Development (TDD), configuração e uso do JUnit 5, asserções para validar comportamento e práticas recomendadas para escrever testes de alta qualidade. Quer você seja um iniciante em busca de compreender o básico ou um especialista em busca de aprimorar suas habilidades, você encontrará informações valiosas sobre testes Java.

O que é teste de unidade Java?

O objetivo do teste unitário é isolar “unidades” de código e testá-las para garantir que estejam funcionando conforme o esperado. Uma “unidade” é a menor parte testável de um aplicativo, normalmente um único método ou classe. Dessa forma, quando um teste falha, é fácil identificar qual parte ou “unidade” não está funcionando conforme o esperado.

Mas antes de nos aprofundarmos nas etapas específicas envolvidas nos testes unitários, vamos ver por que devemos criar testes unitários.

Por que escrever testes unitários?

Os desenvolvedores Java geralmente precisam testar o código manualmente para ver se ele funciona conforme o esperado. Escrever testes unitários ajuda a automatizar esse processo e garante que os mesmos testes sejam executados no mesmo ambiente sob as mesmas condições iniciais.

Os testes de unidade têm uma série de vantagens, incluindo:

  1. Solução de problemas fácil: Os testes JUnit revelarão quando seu código não está funcionando conforme o esperado. Isso torna mais fácil identificar bugs ou problemas importantes antes que eles se agravem e se infiltrem em suas compilações de produção.
  2. Habilite a refatoração de código: Os testes de unidade fornecem uma rede de segurança quando seu código muda, para que você possa refatorá-lo e modificá-lo com a confiança de que não introduzirá novos bugs em seu software.
  3. Melhore a qualidade do código: Os testes de unidade incentivam os desenvolvedores a escrever códigos mais modulares, testáveis ​​e de fácil manutenção.

Embora escrever testes de unidade possa consumir muito tempo inicialmente, ele pode, em última análise, reduzir o tempo geral de desenvolvimento, reduzindo o esforço gasto na correção de bugs e no retrabalho do código posteriormente no processo de desenvolvimento.

Desenvolvimento orientado a testes

Test Driven Development é uma prática de desenvolvimento de software em que os desenvolvedores escrevem métodos de teste antes de escrever o código. A ideia é avaliar primeiro o comportamento pretendido. Isto, em muitos casos, facilita a implementação do comportamento real. Também é mais difícil introduzir bugs. Você pode corrigir quaisquer bugs que tenham aparecido escrevendo testes adicionais que expõem o comportamento defeituoso do código.

O processo TDD normalmente envolve três etapas:

  • Escreva testes com falha: Descreva o comportamento pretendido do seu aplicativo e escreva casos de teste com base nisso. Espera-se que os testes falhem.
  • Escreva o código: A próxima etapa é escrever algum código para fazer os testes passarem. O código é escrito apenas para atender aos requisitos do teste e nada mais.
  • Reestruturação: Procure maneiras de melhorar o código e ao mesmo tempo manter sua funcionalidade. Isso pode incluir simplificar o código, remover duplicações ou melhorar seu desempenho.

Instalação do JUnit 5

Agora que cobrimos a importância e o processo do Desenvolvimento Orientado a Testes, podemos explorar como configurar o JUnit 5, uma das estruturas de teste Java mais populares.

Maven

Para instalar o JUnit 5 no Maven, adicione as seguintes dependências no arquivo pom.xml.

<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency><!-- For running parameterized tests -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependencies>

Gradle

Para instalar e configurar o JUnit 5 no Gradle, adicione as seguintes linhas ao seu arquivo build.gradle.

test { useJUnitPlatform  }

dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
}

Pacotes JUnit

Tanto org.junit quanto org.junit.jupiter.api são pacotes de teste de unidade Java que fornecem suporte para escrever e executar testes.

Mas org.junit é a estrutura de teste mais antiga, introduzida com o JUnit 4, enquanto org.junit.jupiter.api é a estrutura de teste de software Java mais recente introduzida com o JUnit 5. Este último se baseia no JUnit 4 e adiciona novos recursos e funcionalidades. O framework JUnit 5 possui suporte para testes parametrizados, testes paralelos e lambdas, entre outros recursos. Para nossos propósitos, usaremos JUnit 5.

Como escrever testes unitários

Podemos marcar um método como teste adicionando a anotação @Test. O método marcado para teste deve ser público.

No JUnit 5, existem duas maneiras de usar métodos de asserção como assertEquals, assertTrue e assim por diante: importação estática e importação regular.

Uma importação estática permite usar apenas membros estáticos (como métodos) de uma classe sem especificar o nome da classe. No JUnit, as importações estáticas são comumente usadas para métodos de asserção. Por exemplo, em vez de escrever Assert.assertEquals(expected, actual), você pode usar assertEquals(expected, actual) diretamente após usar uma instrução de importação estática.

import static org.junit.jupiter.api.Assert.*;
public class MainTest {
@Test
public void twoPlusTwoEqualsFalse  {
int result = 2 + 2;
assertEquals(4, result);
}
}

Asserções

JUnit 5 fornece vários métodos de asserção integrados que podem ser usados ​​para verificar o comportamento do código em teste. Uma afirmação é simplesmente um método que compara a saída de uma unidade de teste com um resultado esperado.

Ao longo deste artigo, veremos vários métodos de teste. Mantendo em mente as ideias do desenvolvimento orientado a testes, não examinaremos a implementação do código em nenhum desses casos. Em vez disso, discutiremos o comportamento pretendido e os casos extremos (se houver) e escreveremos testes JUnit com base nisso.

Assert.assertEquals e Assert.assertNotEquals

O método assertEquals é usado para verificar se dois valores são iguais ou não. O teste será aprovado se o valor esperado for igual ao valor real.

No exemplo, estamos testando um método “add”, que pega dois inteiros e retorna sua soma.

@Test
void threePlusFiveEqualsEight  {
Calculator calculator = new Calculator ;// syntax: assertEquals(expected value, actual value, message);
assertEquals(8, calculator.add(3, 5));
}

Ao comparar objetos, o método assertEquals usa o método “equals” do objeto para determinar se eles são iguais. Se o método “equals” não for substituído, só então ele realizará uma comparação de referência. Por exemplo, chamar assertEquals em duas strings chamará o método string.equals(string).

Tenha isso em mente porque os arrays não substituem o método “equals”. Chamar array1.equals(array2) comparará apenas suas referências. Portanto, você não deve usar assertEquals para comparar arrays ou qualquer objeto que não substitua o método equals. Se você quiser comparar arrays, use Arrays.equals(array1, array2), e se quiser testar a igualdade de arrays, use o método assertArrayEquals.

Assert.assetSame

Este método compara as referências de dois objetos ou valores. O teste é aprovado quando os dois objetos têm as mesmas referências. Caso contrário, ele falhará.

Assert.assertTrue e Assert.assertFalse

O método assertTrue verifica se uma determinada condição é verdadeira ou não. O teste só será aprovado se a condição for verdadeira. Aqui estamos testando o método mod , que retorna o módulo de um número.

@Test
void mustGetPositiveNumber  {
// syntax: assertTrue(condition)
assertTrue(calculator.mod(-32) >= 0)
}

Da mesma forma, o método assertFalse passa no teste somente quando a condição é falsa.

Assert.assertNull e Assert.assertNonNull

Como você deve ter adivinhado, o método assertNull espera um valor nulo. Da mesma forma, o método assertNonNull espera qualquer valor que não seja nulo.

Assert.assertArrayEquals

Anteriormente, mencionamos que usar assertEquals em arrays não produz o resultado pretendido. Se você quiser comparar dois arrays elemento por elemento, use assertArrayEquals.

Jogos de teste

Os acessórios de teste JUnit são um conjunto de objetos, dados ou código usado para preparar um ambiente de teste e fornecer um ponto de partida conhecido para o teste. Isso inclui as tarefas de preparação e limpeza necessárias para testar uma unidade específica de código.

Antes de cada e depois de cada

A anotação @BeforeEach no JUnit é usada para marcar um método que deve ser executado antes de cada teste em uma classe de teste. A anotação @BeforeEach é usada para preparar o ambiente de teste ou configurar quaisquer recursos necessários antes da execução de cada caso de teste.

Nos exemplos anteriores, em vez de instanciar o objeto Calculadora dentro de cada método de teste, podemos instanciá-lo em um método separado que será chamado antes que o executor de teste execute um teste.

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class CalculatorTest {    Calculator calculator;
@BeforeEach
void setUp  {
calculator = new Calculator ;
}
@Test
void twoPlusTwoEqualsFour  {
assertEquals(4, calculator.add(2, 2));
}
}

A anotação @AfterEach no JUnit é usada para marcar um método que deve ser executado após cada teste em uma classe de teste. A anotação @AfterEach pode ser usada para limpar quaisquer recursos (como bancos de dados ou conexões de rede) ou redefinir estados que foram criados durante a execução do caso de teste.

Antes de tudo e depois de tudo

As anotações @BeforeAll e @AfterAll no JUnit são usadas para marcar métodos que deve ser executado uma vez antes e depois de todos os casos de teste executados.

O principal caso de uso do método @BeforeAll é configurar quaisquer recursos globais ou inicializar qualquer estado compartilhado que precise estar disponível para todos os casos de teste na classe. Por exemplo, se uma classe de teste requer uma conexão com o banco de dados, o método @BeforeAll pode ser usado para criar uma única conexão com o banco de dados que pode ser compartilhada por todos os casos de teste.

Mais algumas afirmações

Depois de abordar anotações como @AfterEach, @BeforeAll e @AfterAll, podemos agora mergulhar em algumas asserções avançadas em JUnit, começando com técnicas para testar exceções.

Testando exceções

Para verificar se um trecho de código lançará uma exceção ou não, você pode usar assertThrows, que toma a referência de classe da exceção esperada como o primeiro argumento e o trecho de código que você deseja testar como o segundo argumento.

Agora, digamos que queremos testar o método “dividir” da nossa classe Calculadora, que pega dois números inteiros, divide o primeiro número pelo segundo número e retorna um valor duplo. No entanto, lança uma exceção (ArithmeticException) se o segundo argumento for zero. Podemos testar isso usando o método assertThrows.

    @Test
void testDivision  {
assertThrows(RuntimeException.class,   -> calculator.divide(32, 0));
}

Se você executar o teste acima, notará que o teste foi aprovado. Conforme mencionado anteriormente, o método divide retornará uma ArithmeticException, mas não estamos verificando isso. O código acima funciona porque assertThrows apenas verifica se uma exceção será lançada, independentemente do tipo.

Use assertThrowsExactly para esperar um erro de tipo fixo. Nesse caso, é melhor usar assertThrowsExactly.

    @Test
void testDivision  {
assertThrowsExactly(ArithmeticException.class,   -> calculator.divide(32, 0));
}

O método assertNotThrows recebe um código executável como argumento e testa se o código lança alguma exceção. O teste será aprovado se nenhuma exceção for lançada.

Teste de tempos limite

O método assertTimeout permite testar se um bloco de código é concluído dentro de um limite de tempo especificado. Aqui está a sintaxe de assertTimeout:

assertTimeout (tempo limite de duração, executável executável)

assertTimeout executa o código em teste em um thread separado e, se o código for concluído dentro do limite de tempo especificado, a asserção será aprovada. Se o código demorar mais que o limite de tempo especificado para ser concluído, a asserção falhará e o teste será marcado como uma falha.

    @Test
    void testSlowOperation  {
        assertTimeout(Duration.ofSeconds(1),   -> {
            Thread.sleep(500); // simulate a slow operation
        });
    }

A afirmação assertTimeoutPreemptively é um método que permite testar se um bloco de código é concluído dentro de um período especificado, assim como o assertTimeout. A única diferença é que assertTimeoutPreemptive interrompe a execução quando o limite de tempo é excedido.

Testes Dinâmicos

Testes dinâmicos em JUnit são testes gerados em tempo de execução em vez de serem predefinidos. Eles permitem que os desenvolvedores gerem testes programaticamente com base nos dados de entrada. Os testes dinâmicos são implementados usando a anotação @TestFactory. O método anotado @TestFactory deve retornar um Stream, Collection ou Iterator do tipo genérico DynamicTest.

Aqui, estamos testando um método de subtração que retorna a diferença entre o primeiro argumento e o segundo.

@TestFactory
    Stream<DynamicTest> testSubtraction  {
        List<Integer> numbers = List.of(3, 7, 14, 93);

        return numbers.stream .map((number) -> DynamicTest.dynamicTest("Test: " + number,   -> 
            assertEquals(number - 4, calculator.subtract(number, 4));
        ));
    }

Testes parametrizados

Os testes parametrizados permitem escrever um único método de teste e executá-lo várias vezes com argumentos diferentes. Isso pode ser útil quando você deseja testar um método com diferentes valores de entrada ou combinações de valores.

Para criar um teste parametrizado no JUnit 5, você pode usar a anotação @ParameterizedTest e fornecer argumentos usando anotações como @ValueSource, @CsvSource, @MethodSource, @ArgumentsSources e assim por diante.

Passando um argumento

A anotação @ValueSource recebe uma matriz de valores únicos de qualquer tipo. No exemplo abaixo, estamos testando uma função que verifica se um determinado número é ímpar ou não. Aqui, estamos usando as anotações @ValueSource para obter uma lista de argumentos. O executor de teste executa o teste para cada valor fornecido.

    @ParameterizedTest
    @ValueSource(ints = {3, 9, 77, 191})
    void testIfNumbersAreOdd(int number) {
        assertTrue(calculator.isOdd(number), "Check: " + number);
    }

Passando vários argumentos

A anotação @CsvSource recebe uma lista de argumentos separados por vírgula como entrada, onde cada linha representa um conjunto de entradas para o método de teste. Aqui, no exemplo abaixo, estamos testando um método de multiplicação que retorna o produto de dois inteiros.

    @ParameterizedTest
    @CsvSource({"3,4", "4,14", "15,-2"})
    void testMultiplication(int value1, int value2) {
        assertEquals(value1 * value2, calculator.multiply(value1, value2));
    }

Passando valores nulos e vazios

A anotação @NullSource fornece um único argumento nulo. O método de teste é executado uma vez com um argumento nulo.

A anotação @EmptySource fornece um argumento vazio. Para strings, esta anotação fornecerá uma string vazia como argumento.

Além disso, se você quiser usar argumentos nulos e vazios, use a anotação @NullAndEmptySource.

Passando Enums

Quando a anotação @EnumSource é usada com um método de teste parametrizado, o método é executado uma vez para cada constante enum especificada.

No exemplo abaixo, o executor de teste executa o método testWithEnum para cada valor do enum.

enum Color {
    RED, GREEN, BLUE
}

@ParameterizedTest
@EnumSource(Color.class)
void testWithEnum(Color color) {
    assertNotNull(color);
}

Por padrão, @EnumSource inclui todas as constantes definidas no tipo de enum especificado. Você também pode personalizar a lista de constantes especificando um ou mais dos atributos a seguir.

O atributo name é usado para especificar os nomes de constantes a serem incluídos ou excluídos, e o atributo mode é usado para especificar se os nomes devem ser incluídos ou excluídos.

enum ColorEnum {
        RED, GREEN, BLUE
    }

    @ParameterizedTest
    @EnumSource(value = ColorEnum.class, names = {"RED", "GREEN"}, mode = EnumSource.Mode.EXCLUDE)
    void testingEnums(ColorEnum colorEnum) {
        assertNotNull(colorEnum);
    }

No exemplo acima, o caso de teste será executado apenas uma vez (para ColorEnum.BLUE).

Passando argumentos do arquivo

No exemplo abaixo, @CsvFileSource é usado para especificar um arquivo CSV (test-data.csv) como fonte de argumento para o método testWithCsvFileSource . O arquivo CSV contém três colunas, que correspondem aos três parâmetros do método.

//    Contents of the .csv file
//    src/test/resources/test-data.csv
//    10, 2, 12
//    14, 3, 17
//    5, 3, 8

@ParameterizedTest
@CsvFileSource(resources = "/test-data.csv")
void testWithCsvFileSource(String input1, String input2, String expected) {
        int iInput1 = Integer.parseInt(input1);
        int iInput2 = Integer.parseInt(input2);
        int iExpected = Integer.parseInt(expected);
        assertEquals(iExpected, calculator.add(iInput1, iInput2));
}

O atributo resources especifica o caminho para o arquivo CSV relativo ao diretório src/test/resources em seu projeto. Você também pode usar um caminho absoluto, se necessário.

Observe que os valores no arquivo CSV são sempre tratados como strings. Talvez seja necessário convertê-los para os tipos apropriados em seu método de teste.

Passando valores de um método

A anotação @MethodSource é usada para especificar um método como fonte de argumento para um método de teste parametrizado. Isso pode ser útil quando você deseja gerar casos de teste com base em um algoritmo ou estrutura de dados customizada.

No exemplo abaixo, estamos testando o método isPalindrome que recebe um número inteiro como entrada e verifica se o número inteiro é palíndromo ou não.

static Stream<Arguments> generateTestCases  {
    return Stream.of(
        Arguments.of(101, true),
        Arguments.of(27, false),
        Arguments.of(34143, true),
        Arguments.of(40, false)
    );
}

@ParameterizedTest
@MethodSource("generateTestCases")
void testWithMethodSource(int input, boolean expected) {
    // the isPalindrome(int number) method checks if the given
    // input is palindrome or not
    assertEquals(expected, calculator.isPalindrome(input));
}

Argumentos personalizados

O @ArgumentsSource (não deve ser confundido com ArgumentsSources) é uma anotação que pode ser usada para especificar um provedor de argumento personalizado para um método de teste parametrizado. O provedor de anotação customizada é uma classe que fornece argumentos para o método de teste. A classe deve implementar a interface ArgumentsProvider e substituir seu método provideArguments .

Considere o seguinte exemplo:

static class StringArgumentsProvider implements ArgumentsProvider {
        String  fruits = {"apple", "mango", "orange"};

        @Override
        public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {
            return Stream.of(fruits).map(Arguments::of);
        }
    }

    @ParameterizedTest
    @ArgumentsSource(StringArgumentsProvider.class)
    void testWithCustomArgumentsProvider(String fruit) {
        assertNotNull(fruit);
    }

Neste exemplo, StringArgumentsProvider é um provedor de argumentos customizados que fornece strings como argumentos de teste. O provedor implementa a interface ArgumentsProvider e substitui seu método provideArguments para retornar um fluxo de argumentos.

Você pode usar a anotação @ArgumentsSources para especificar diversas fontes de argumentos para um único método de teste parametrizado.

Testes aninhados

No JUnit 5, classes de teste aninhadas são uma forma de agrupar testes relacionados e organizá-los em uma estrutura hierárquica. Cada classe de teste aninhada pode conter sua própria configuração, desmontagem e testes.

Para definir uma classe de teste aninhada, use a anotação @Nested antes de uma classe interna. O interior não deve ser estático.

class ExampleTest {
    
    @BeforeEach
    void setup1  {}

    @Test
    void test1  {}

    @Nested
    class NestedTest {

        @BeforeEach
        void setup2  {}

        @Test
        void test2  {}

        @Test
        void test3  {}
    }
}

O código será executado na seguinte ordem.

setup1  -> test1  -> setup1  -> setup2  -> test2  -> setup1  -> setup2  -> test3 

Assim como uma classe de teste pode conter classes de teste aninhadas, uma classe de teste aninhada também pode conter suas próprias classes de teste aninhadas. Isso permite criar uma estrutura hierárquica para seus testes, facilitando a organização e a manutenção do código de teste.

Suíte de teste

JUnit Test Suites são uma forma de organizar seus testes. Embora os testes aninhados sejam uma ótima maneira de organizar testes, à medida que a complexidade de um projeto aumenta, fica cada vez mais difícil mantê-los. Além disso, antes de executar qualquer método de teste aninhado, todos os equipamentos de teste são executados primeiro, o que pode ser desnecessário. Portanto, usamos suítes de testes para organizar nossos testes regularmente.

Para usar os conjuntos de testes JUnit, primeiro crie uma nova classe (digamos, ExampleTestSuite). Em seguida, adicione a anotação @RunWith(Suite.class) para informar ao executor de testes Junit para usar Suite.class para executar os testes. O executor Suite.class no JUnit permite executar várias classes de teste como um conjunto de testes inteiro. Em seguida, especifique as classes que deseja executar usando a anotação @SuiteClasses.

import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

@RunWith(Suite.class)
@SuiteClasses({
    CalculatorTest.class,
    CalculatorUtilsTest.class
})
public class CalculatorTestSuite {}

Melhores práticas para escrever testes melhores

Agora que exploramos afirmações específicas, devemos abordar as melhores práticas que podem maximizar a eficácia dos testes. Uma diretriz fundamental é manter os testes simples e focados, mas há considerações adicionais. Vamos mergulhar em alguns princípios-chave a serem seguidos ao escrever testes unitários robustos e eficientes.

  • Escreva testes simples e focados: Os testes unitários devem ser simples e focados em testar um aspecto do código por vez. Deve ser fácil de entender e manter e fornecer feedback claro sobre o que está sendo testado.
  • Use nomes de testes descritivos: Os nomes dos testes devem ser descritivos e fornecer informações claras sobre o que está sendo testado. Isso ajuda a tornar o conjunto de testes mais legível e compreensível. Para nomear um teste, use a anotação @DisplayName.
@Test
    @DisplayName("Checking nine plus seven equals sixteen")
    void twoPlusTwoEqualsFour  {
        assertEquals(16, calculator.add(9,7));
    }
  • Usando valores aleatórios em tempo de execução: A geração de valores aleatórios em tempo de execução não é recomendada para testes de unidade. O uso de valores aleatórios pode ajudar a garantir que o código testado seja robusto e possa lidar com uma ampla variedade de entradas. Valores aleatórios podem ajudar a revelar casos extremos e outros cenários que podem não ser aparentes em um caso de teste estático. No entanto, o uso de valores aleatórios também pode tornar os testes menos confiáveis ​​e repetíveis. Se o mesmo teste for executado várias vezes, poderá produzir resultados diferentes a cada vez, o que pode dificultar o diagnóstico e a correção de problemas. Se forem utilizados valores aleatórios, é importante documentar a semente utilizada para gerá-los para que os testes possam ser reproduzidos.
  • Nunca teste os detalhes da implementação: Os testes unitários devem se concentrar em testar o comportamento de uma unidade ou componente, e não em como ele é implementado. Testar detalhes de implementação pode tornar os testes frágeis e difíceis de manter.
  • Casos extremos: Casos extremos são casos em que seu código pode falhar. Por exemplo, se você estiver lidando com objetos, um caso extremo comum é quando o objeto é nulo. Certifique-se de cobrir todos os casos extremos ao escrever os testes.
  • Padrão Arrange-Act-Assert (AAA): O padrão AAA é um padrão útil para estruturar testes. Nesse padrão, a fase Arrange configura os dados e o contexto do teste, a fase Act executa a operação que está sendo testada e a fase Assert verifica se os resultados esperados foram obtidos.

Mockito

Mockito é uma estrutura de simulação Java de código aberto que permite criar e usar objetos simulados em testes de unidade. Objetos simulados são usados ​​para simular objetos reais no sistema que são difíceis de testar isoladamente.

Instalação

Para adicionar mockito ao seu projeto, adicione a seguinte dependência no pom.xml.

<!--  -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.3.0</version>
    <scope>test</scope>
</dependency>

Se você estiver usando Gradle, adicione o seguinte ao seu build.gradle.

repositories { mavenCentral  }
dependencies { testImplementation "org.mockito:mockito-core:3.+" }

Usando objetos simulados

Nos testes unitários, queremos testar o comportamento de uma unidade de código independentemente do resto do sistema. No entanto, às vezes um módulo de código depende de outros módulos ou de algumas dependências externas que são difíceis ou impossíveis de testar isoladamente. Neste caso, utilizamos objetos mock para simular o comportamento dessas dependências e isolar o módulo em teste.

No exemplo abaixo, temos uma classe User que queremos testar. A classe User depende de uma classe UserService responsável por buscar dados de um banco de dados. A classe UserService possui um método chamado getUserById que busca informações sobre um usuário em um banco de dados e as retorna.

public class User {
  private final int id;
  private final UserService userService;

  public User(int id, UserService userService) {
    this.userService = userService;
    this.id = id;
  }

  public String getName  {
    UserInfo info = userService.getUserById(id);
    return info.getName ;
  }
}

public class UserService {
  public UserInfo getUserById(int id) {
    // retrieve user information from a database
  }
}

Para testar a unidade do método getName da classe User, precisamos testá-lo isoladamente da classe UserService e do banco de dados.

Uma maneira de fazer isso é usar um objeto simulado para simular o comportamento da classe UserService. Aqui está um exemplo de como fazer isso usando o Mockito:

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

@Test
public void testGetName  {
  UserService userService = Mockito.mock(UserService.class);

  UserInfo info = new UserInfo(123, "John");
  Mockito.when(userService.getUserById(123)).thenReturn(entity);

  User user = new User(123, userService);

  String name = user.getName ;
  assertEquals("John", name);

  Mockito.verify(userService).getUserById(123);
}

No exemplo acima, estamos criando um objeto simulado para a classe UserService usando o método Mockito.mock . Estamos então definindo o comportamento do objeto simulado usando o método Mockito.when , que especifica que quando o método getUserById é chamado com o argumento 123, o objeto simulado deve retornar um objeto UserEntity com o nome “John. ”

Em seguida, criamos um objeto User com o UserService simulado e testamos o método getName . Por fim, verificamos se o objeto mock foi usado corretamente usando o método Mockito.verify , que verifica se o método getUserById foi chamado com o argumento 123.

Usar um objeto simulado dessa forma nos permite testar o comportamento do método getName isoladamente da classe UserService e do banco de dados, garantindo que quaisquer erros ou bugs estejam relacionados apenas ao comportamento da própria classe User.

Estruturas de teste Java

JUnit é de longe a escolha mais popular quando se trata de testar estruturas. No entanto, existem muitas outras opções. Aqui estão alguns deles:

  1. TesteNG: TesteNG é outra estrutura de teste Java popular que oferece suporte a uma ampla variedade de cenários de teste, incluindo testes unitários, testes funcionais e testes de integração. Ele fornece recursos avançados como testes paralelos, dependências de teste e testes baseados em dados.
  2. AfirmarJ: AfirmarJ é uma biblioteca de asserções Java que fornece uma API fluente para definir asserções. Ele fornece uma ampla variedade de asserções para testar diferentes tipos de objetos e oferece suporte a asserções personalizadas.
  3. Hamcrest: Hamcrest é uma biblioteca de asserções Java que fornece uma ampla variedade de matchers para testar diferentes tipos de objetos. Ele permite que os desenvolvedores escrevam testes mais expressivos e legíveis usando asserções de linguagem natural.
  4. Selênio: Selênio é uma estrutura de teste Java para testar aplicativos da web. Ele permite que os desenvolvedores escrevam testes automatizados para aplicações web usando uma variedade de linguagens de programação, incluindo Java.
  5. Pepino: Pepino é uma estrutura de teste Java que permite aos desenvolvedores escrever testes automatizados em um estilo de desenvolvimento orientado a comportamento (BDD). Ele fornece uma sintaxe de linguagem simples e natural para definir testes, facilitando a escrita de testes fáceis de ler e entender.

Conclusão

Neste artigo, cobrimos tudo que você precisa saber para começar a fazer testes unitários usando JUnit e Mockito. Também discutimos os princípios do desenvolvimento orientado a testes e por que você deve segui-lo.

Ao adotar uma abordagem de desenvolvimento orientado a testes, você pode garantir que seu código se comporte conforme planejado. Mas, como qualquer prática de desenvolvimento de software, o TDD tem seus prós e contras, e sua eficácia dependerá do projeto e da equipe específicos. Para projetos maiores, o envolvimento de serviços de desenvolvimento Java pode fornecer experiência em testes para implementar adequadamente o TDD com base em suas necessidades.

Em última análise, a decisão de usar TDD deve levar em consideração os objetivos do projeto, as habilidades da equipe e se recursos externos de teste Java podem ser benéficos. Com o entendimento correto das vantagens e desvantagens do TDD, até mesmo equipes inexperientes podem colher os benefícios de qualidade de uma metodologia de teste primeiro.

Se você gostou disso, não deixe de conferir alguns de nossos outros artigos sobre Java.

  • 8 melhores IDEs Java e editores de texto
  • 6 melhores estruturas Java GUI
  • 7 melhores bibliotecas de aprendizado de máquina Java
  • As 5 principais ferramentas de construção Java comparadas
  • Listadas as 9 melhores ferramentas de análise de código estático Java
  • Simultaneidade Java: domine a arte do multithreading

Perguntas frequentes

Como você lida com dependências ao configurar testes de unidade em Java?

As dependências são gerenciadas automaticamente por ferramentas de construção como Maven e Gradle. Portanto, é altamente recomendável usá-los.

Você pode usar JUnit para testar código não Java, como JavaScript ou Python?

Não, você não pode usar JUnit para testar código não Java. No entanto, linguagens como Javascript e Python possuem suas próprias estruturas para testes unitários. Por exemplo, Javascript (ReacT) possui Jest e Python possui PyTest para testes unitários.

Quais são algumas armadilhas comuns a serem evitadas ao escrever testes de unidade e como você pode mitigá-las?

Ao escrever testes unitários, certifique-se de que seus testes sejam simples e focados em testar um aspecto do código por vez. Use nomes descritivos e agrupe testes semelhantes. Tente cobrir todos os casos extremos.

Fonte: BairesDev

Conteúdo Relacionado

O Rails 8 sempre foi um divisor de águas...
Os genéricos são uma característica poderosa em Java que...
A GenAI está transformando a força de trabalho com...
Entenda o papel fundamental dos testes unitários na validação...
Aprenda como os testes de carga garantem que seu...
Aprofunde-se nas funções complementares dos testes positivos e negativos...
Vídeos deep fake ao vivo cada vez mais sofisticados...
Entenda a metodologia por trás dos testes de estresse...
Descubra a imprevisibilidade dos testes ad hoc e seu...
A nomeação de Nacho De Marco para o Fast...
Aprenda como os processos baseados em IA aprimoram o...
Java e JavaScript são duas das linguagens de programação...
Introdução Quando se trata de desenvolvimento de software, a...
Os desenvolvedores Java enfrentam uma variedade de erros relacionados...
Neste artigo, discutiremos os benefícios de gerar imagens em...
O Java 23 finalmente foi lançado, e podemos começar...
Milhares de sites são criados todos os dias. Não...
Os recursos de linguagem que serão incluídos na próxima...
Voltar para o blog

Deixe um comentário

Os comentários precisam ser aprovados antes da publicação.