Artigo original: How to Write Unit Tests in Java

Digamos que você está desenvolvendo uma aplicação. Depois de longas horas programando, você consegue criar algumas funcionalidades legais. Agora, você quer ter certeza de que as funcionalidades estão do jeito que você quer.

Isso envolve testar se cada parte do código funciona como esperado. Esse procedimento é conhecido como teste unitário. Diferentes linguagens fornecem seus próprios frameworks para testes.

Neste artigo, vou mostrar como escrever testes unitários em Java. Primeiro, explicarei o que o teste envolve e alguns conceitos que você precisará saber. Então, mostrarei alguns exemplos para ajudar você a entender melhor.

Para este artigo, assumo que você esteja familiarizado com o Java e com o IDE IntelliJ.

Configuração do projeto

Para este projeto, usarei o IDE IntelliJ. Se você não o tiver, siga este guia (em inglês) para instalar o IDE.

Neste projeto, usaremos as bibliotecas JUnit e Mockito para testes. Elas são as bibliotecas mais usadas para testes em Java. Você entenderá como essas bibliotecas são usadas à medida que avança pelo artigo.

Para configurar o JUnit, siga as etapas abaixo, conforme descrito neste guia (em inglês):

  1. No menu principal, selecione File > New > Project.
  2. Selecione New Project. Especifique um nome para o projeto – eu darei o nome de junit-testing-tutorial.
  3. Selecione Maven como a ferramenta de build e, na linguagem, selecione Java.
  4. Na lista JDK, selecione o JDK que você deseja usar no projeto.
  5. Clique em Create.
  6. Abra o arquivo pom.xml no diretório raiz do seu projeto.
  7. No pom.xml, dentro das tags <dependencies></dependencies>, pressione Alt + Insert (ou Cmd + Insert no macOS), e selecione Generate Dependency.
  8. Isso abrirá uma janela de ferramentas. Digite org.junit.jupiter:junit-jupiter no campo de pesquisa. Localize a dependência necessária e clique no botão Add.
  9. Agora, clique em Load Maven Changes na notificação que aparece no canto superior direito no editor.

Agora, para configurar o Mockito, adicione essas duas dependências no seu pom.xml:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>5.2.0</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.2.0</version>
    <scope>compile</scope>
</dependency>

Observação: a versão pode ser diferente, dependendo da época em que você está lendo este artigo.

Para completar a configuração, crie uma classe Welcome e defina sua função principal lá.

Screenshot-2023-03-12-at-6.14.33-PM
Estrutura de pastas e o método principal

O que é um teste unitário?

Os testes unitários envolvem testar cada componente do seu código para ver se ele funciona como esperado. Eles isolam cada método individual do seu código e executam testes nele. Os testes unitários ajudam a garantir que seu software está funcionando como esperado antes de ser lançado.

Como desenvolvedor, você escreverá testes unitários assim que terminar de escrever um trecho de código. Você pode se perguntar se essa não é a tarefa de um testador. De certo modo, sim. Um testador é responsável por testar o software. No entanto, examinar cada linha de código coloca muita pressão no testador. Portanto, é uma boa prática para os desenvolvedores escreverem testes para seu próprio código também.

O objetivo do teste unitário é garantir que qualquer nova funcionalidade não interrompa a funcionalidade existente. Também ajuda a identificar quaisquer problemas ou bugs mais cedo no processo de desenvolvimento e ajuda a garantir que o código atenda aos padrões de qualidade definidos pela organização.

O que fazer e o que não fazer ao realizar testes unitários

Lembre-se das seguintes diretrizes ao escrever testes para seus métodos:

  • Teste se a saída esperada de um método corresponde à saída real.
  • Teste se as funções chamadas dentro do método estão ocorrendo o número desejado de vezes.
  • Não tente testar código que não faça parte do método que está sendo testado.
  • Não faça chamadas de API, conexões de banco de dados ou solicitações de rede ao escrever seus testes.

Agora, vamos ver alguns conceitos que você precisa saber antes de começarmos a escrever testes.

Afirmações

As afirmações (em inglês, assertions) determinam se seu teste é aprovado ou reprovado. Elas comparam o valor de retorno esperado de um método com o valor real. Há uma série de afirmações que você pode fazer no final do seu teste.

A classe Assertions no JUnit consiste em métodos estáticos que fornecem várias condições para decidir se o teste é aprovado ou não. Veremos esses métodos à medida que eu o guie por cada exemplo.

Mocking

A classe cujos métodos você está testando pode ter algumas dependências externas. Como mencionado anteriormente, você não deve tentar testar código que não faça parte da função que está sendo testada.

Nos casos em que sua função usa uma classe externa, porém, é uma boa prática fazer um mock dessa classe, ou seja, ter valores de mock-up (algo como "simulações", em português) em vez dos valores reais. Usaremos a biblioteca Mockito para esse fim.

Stubbing de método

As dependências externas podem não se limitar apenas às classes, mas também a certos métodos. O stubbing de método deve ser feito quando sua função está chamando uma função externa em sua implementação. Nesse caso, você faz com que essa função retorne o valor que você deseja em vez de chamar o método real.

Por exemplo, o método que você está testando (A) está chamando um método externo (B) em sua implementação. B faz uma consulta ao banco de dados, buscando todos os alunos com notas maiores que 80. Fazer uma chamada real ao banco de dados não é uma boa prática aqui. Portanto, você faz um stub do método e o faz retornar uma lista fictícia de alunos que você precisa para testar.

Você entenderá isso melhor com exemplos. Existem muitos outros conceitos que fazem parte dos testes em Java. Por enquanto, esses três são suficientes para você começar.

Passos a serem realizados durante os testes

  1. Inicialize os parâmetros necessários que você precisará para executar o teste.
  2. Crie objetos de mock-up e faça um stub de qualquer método se necessário.
  3. Chame o método que você está testando com os parâmetros que você inicializou na etapa 1.
  4. Adicione uma afirmação para verificar o resultado do seu teste. Isso decidirá se o teste é aprovado.

Você entenderá essas etapas melhor com exemplos. Vamos começar com um teste básico primeiro.

Como escrever o primeiro teste

Vamos escrever uma função simples que compara dois números. Ela retorna 1 se o primeiro número for maior que o segundo ou -1, do contrário.

Colocaremos essa função dentro de uma classe Basics:

public class Basics {
    public int compare(int n1, int n2) {
        if (n1 > n2) return 1;
        return -1;
    }
}

Bem simples! Vamos escrever o teste para essa classe. Todos os seus testes devem estar localizados dentro da pasta test.

Screenshot-2023-03-13-at-5.12.04-PM
Estrutura de pastas

Dentro da pasta test, crie uma classe BasicTests, onde você escreverá seus testes para essa classe. O nome da classe não importa, mas é uma boa prática segregar os testes de acordo com cada classe. Além disso, siga uma estrutura de pasta semelhante à do seu código principal.

public class BasicTests {
    // Seus testes vêm aqui
}

Os testes unitários são basicamente um conjunto de métodos que você define para testar cada método da sua classe. Dentro da classe acima, crie um método compare(), com um tipo de retorno de void. Novamente, você pode nomear o método como quiser.

@Test
public void compare() {

}

A anotação @Test indica que esse método deve ser executado como um caso de teste.

Agora, para testar o método, você precisa criar o objeto da classe acima e chamar o método passando alguns valores.

Basics basicTests = new Basics();
int value = basicTests.compare(2, 1);

Agora, use o método assertEquals() da classe Assertions para verificar se o valor retornado corresponde ao valor esperado.

Assertions.assertEquals(1, value);

Nosso teste deve ser aprovado, pois o valor retornado pelo método corresponde ao valor esperado. Para verificar, execute o teste clicando com o botão direito na seta verde ao lado do método de teste.

Screenshot-2023-03-13-at-5.30.14-PM
Executando um teste

Os resultados do seu teste serão mostrados abaixo.

Screenshot-2023-03-13-at-5.32.06-PM
Resultados do teste

Mais exemplos de teste

No teste acima, apenas testamos um cenário. Quando há ramificações na função, você precisa escrever testes para cada condição. Vamos introduzir algumas ramificações adicionais na função acima.

public int compare(int n1, int n2) {
    if (n1 > n2) return 1;
    else if (n1 < n2) return -1;
    return 0;
}

Já testamos o primeiro ramo, então vamos escrever testes para os outros dois.

@Test
@DisplayName("Primeiro número é menor que o segundo")
public void compare2() {
    Basics basicTests = new Basics();
    int value = basicTests.compare(2, 3);
    Assertions.assertEquals(-1, value);
}

A anotação @DisplayName exibe o texto em vez do nome do método abaixo. Vamos executar o teste.

Screenshot-2023-03-13-at-6.01.15-PM
Teste aprovado

Para o caso em que os dois números são iguais:

@Test
@DisplayName("Primeiro número é igual ao segundo")
public void compare3() {
    Basics basicTests = new Basics();
    int value = basicTests.compare(2, 2);
    Assertions.assertEquals(0, value);
}

Ordenando um array

Agora, vamos escrever o teste para o seguinte código que ordena um array.

public void sortArray(int[] array) {
        int n = array.length;
        for (int i = 0; i < n-1; i++) {
            for (int j = 0; j < n-i-1; j++) {
                if (array[j] > array[j+1]) {
                    int temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                }
            }
        }
    }

Para escrever o teste para isso, seguiremos um procedimento semelhante: chamaremos o método e passaremos um array para ele. Usaremos o assertArrayEquals() para escrever nossa afirmação.

@Test
@DisplayName("Array classificado")
public void sortArray() {
    Basics basicTests = new Basics();
    int[] array = {5, 8, 3, 9, 1, 6};
    basicTests.sortArray(array);
    Assertions.assertArrayEquals(new int[]{1, 3, 5, 6, 8, 9}, array);
}

Um desafio para você: escreva um código que inverta uma string e escreva um caso de teste para esse código.

Como criar mock-ups e stubs para testes

Vimos alguns exemplos básicos de testes unitários onde você fez afirmações simples. No entanto, as funções que você está testando podem conter dependências externas, como classes de modelo e conexões de banco de dados ou de rede.

Você não deve fazer conexões reais em seus testes, pois seria muito demorado. Nesses casos, você faz um mock-up dessas implementações. Vamos ver alguns exemplos de mock-ups.

Fazendo mock-up de uma classe

Vamos ter uma classe User com as seguintes propriedades:

public class User {
    private String username;
    private String password;
    private String role;
    private List<String> posts;    
}

Clique em Alt + Insert (ou Cmd + Insert no macOS) para gerar métodos getters e setters para as propriedades acima.

Screenshot-2023-03-18-at-8.34.34-PM
Gerar opções

Vamos gerar uma nova classe Mocking que use o objeto acima.

public class Mocking {
    User user;

    public void setUser(User user) {
        this.user = user;
    }
}

Essa classe possui um método que atribui determinadas permissões com base na função do usuário. Ela retorna 1 se a permissão for atribuída com sucesso. Do contrário, ela retorna -1.

public int assignPermission() {
        if(user.getRole().equals("admin")) {
            String username = user.getUsername();
            System.out.println("Atribuir permissões especiais para o usuário " + username);
            return 1;
        } else {
            System.out.println("Não é possível atribuir permissão");
            return -1;
        }
    }

Para fins de demonstração, adicionei apenas instruções println(). A implementação real pode envolver a configuração de certas propriedades.

No arquivo de testes, adicionaremos uma anotação @ExtendWith no topo, pois estamos usando o Mockito. Não mostrei as importações aqui, pois o IntelliJ as faz automaticamente.

@ExtendWith(MockitoExtension.class)
public class MockingTests {

}

Então, como escrevemos o teste para o método? Precisaremos fazer um mock-up do objeto User. Você pode fazer isso adicionando uma anotação @Mock ao declarar o objeto.

@Mock
User user;

Você também pode usar o método mock(), pois é semelhante.

User user = mock(User.class);

Vamos escrever o método de teste.

@Test
@DisplayName("Permissão atribuída com sucesso")
public void assignPermissions() {
    Mocking mocking = new Mocking();
    Assertions.assertEquals(1, mocking.assignPermission());
}

Quando você executar o teste, ele lançará uma NullPointerException.

Screenshot-2023-03-18-at-8.51.26-PM
Objeto User é null

Isso ocorre porque o objeto do usuário ainda não foi inicializado. O método que você chamou não conseguiu usar o objeto de mock-up. Para isso, você precisará chamar o método setUser.

mocking.setUser(user);

Agora, o teste fornece o erro abaixo, pois o objeto de mock-up é inicialmente preenchido com valores nulos.

Screenshot-2023-03-18-at-8.53.53-PM
Valor de retorno de getRole() é null

Isso significa que você precisa preencher o objeto de mock-up com valores reais? Não, você só precisa que o método getRole() retorne um valor que não seja null. Para isso, usaremos o stubbing de método.

when(user.getRole()).thenReturn("admin");

Usar when()...thenReturn() diz ao teste para retornar um valor quando um método for chamado. Você deve fazer o stubbing de métodos apenas para objetos de mock-up.

Faremos o mesmo para o método getUsername().

when(user.getUsername()).thenReturn("kunal");

Agora, se você executar o teste, ele será aprovado.

Screenshot-2023-03-18-at-9.00.21-PM
Teste aprovado

Exemplo de stubbing de método

No exemplo acima, eu simplesmente fiz o stubbing dos métodos getter para demonstrar o stubbing de método. Em vez de fazer stubbing dos getters, você pode definir a função e o nome de usuário com um construtor parametrizado ou métodos setter, se eles estiverem disponíveis.

user.setRole("admin");
user.setUsername("kunal");

O que fazer, contudo, se a classe User tiver um método que retorna todas as publicações (em inglês, posts) contendo uma determinada palavra nelas?

public List<String> getAllPostsContainingWord(String word) {
        List<String> filteredPosts = new ArrayList<>();
        for(String post: posts) {
            if(post.contains(word))
                filteredPosts.add(post);
        }
        return filteredPosts;
    }

Queremos que esse método retorne todas as publicações contendo a palavra "incrível". Se você chamar a implementação real desse método, pode levar muito tempo, pois o número de publicações pode ser enorme. Além disso, se você estiver fazendo mocking do objeto User, o array de publicações será nulo.

Nesse caso, você faz um stub do método e o faz retornar a lista que você deseja.

List<String> filteredPosts = new ArrayList<>();
filteredPosts.add("Dia Incrível");
filteredPosts.add("Este lugar é incrível");
when(user.getAllPostsContainingWord("incrível")).thenReturn(filteredPosts);

Stubbing de método em consultas de banco de dados

Vamos ver como testar métodos que envolvem fazer conexões com banco de dados. Primeiro, crie uma classe ApplicationDao que contenha todos os métodos que executam consultas ao banco de dados.

public class ApplicationDao {  }

Defina um método que busca o usuário por id e retorna null se o usuário não for encontrado.

public User getUserById(String id) {
    // Fazer consulta ao banco de dados aqui        
}

Crie outro método para salvar um usuário no banco de dados. Esse método lança uma exceção se o objeto do usuário que você está tentando salvar for null.

public void save(User user) throws Exception {
    // Fazer consulta ao banco de dados aqui
}

Nossa classe de mock-up usará esses métodos para implementar suas próprias funcionalidades. Implementaremos uma função que atualiza o nome de um usuário.

public int updateUsername(String id, String username) throws Exception{
            ApplicationDao applicationDao = new ApplicationDao();
            User user = applicationDao.getUserById(id);
            if(user!=null)
                user.setUsername(username);
            applicationDao.save(user);
            return 1;
    }

A implementação do método é bastante simples. Primeiro, obtenha o usuário por id, altere seu nome de usuário e salve o objeto do usuário atualizado. Escreveremos os casos de teste para esse método.

Existem dois casos que precisamos testar. O primeiro é quando o usuário é atualizado com sucesso. O segundo é quando a atualização falha, ou seja, quando uma exceção é lançada.

Antes de escrever os testes, crie um mock-up do objeto ApplicationDao, pois não queremos fazer conexões reais com o banco de dados.

@Mock
ApplicationDao applicationDao;

Vamos escrever nosso primeiro teste.

@Test
    @DisplayName("Usuário atualizado com sucesso")
    public void updateUsername() throws Exception {
        ...
    }

Crie um objeto do usuário para testar.

User user = new User();
user.setUsername("kunal");

Como estamos chamando um método externo, vamos fazer um stub do método para que ele retorne o objeto User acima.

when(applicationDao.getUserById(Mockito.anyString())).thenReturn(user);

Passe Mockito.anyString() para o método, pois queremos que o stub funcione para qualquer parâmetro de string. Agora, adicione uma afirmação para verificar se o método está funcionando corretamente.

Assertions.assertEquals(1, mocking.updateUsername("3211", "allan"));

O método retorna 1 em caso de atualização bem-sucedida. O teste, então, é aprovado.

Agora, vamos testar outro cenário em que o método falha e lança uma exceção. Simule esse cenário fazendo com que o método getUserById() retorne null.

lenient().when(applicationDao.getUserById(Mockito.anyString())).thenReturn(null);

Esse valor é, então, passado para o método save(), que, por sua vez, lança uma exceção. Em nossa afirmação, usaremos o método assertThrows() para testar se uma exceção foi lançada. Esse método recebe o tipo da exceção e uma expressão lambda como parâmetros.

Assertions.assertThrows(Exception.class, () -> {
            mocking.updateUsername("3412","allan");
        });

Como a exceção é lançada, nosso teste é aprovado.

Screenshot-2023-03-26-at-4.27.07-PM
Testes aprovados

Você pode encontrar o código completo aqui no GitHub.

Conclusão

Como desenvolvedor, escrever testes unitários para seu código é importante. Isso ajuda você a identificar bugs mais cedo no processo de desenvolvimento.

Neste artigo, comecei introduzindo o teste unitário e expliquei três conceitos importantes envolvidos no processo de teste. Isso dará a você uma boa base antes de passar para o código.

Depois disso, mostrei, com exemplos, como você pode testar diferentes cenários usando as mesmas técnicas básicas de teste. Também mostrei como usar classes e métodos mock-ups para testar implementações complexas.