Introdução a Dummies

Ao concluirmos esta aula, nós:

  • Saberemos o que é um Dummy.
  • Compreenderemos quando devemos utilizar Dummies.
  • Saberemos quais técnicas utilizar para criar Dummies de forma expressiva.

[!NOTE] Esta aula assume que você está familiarizado com o Sistema Sob Teste (SUT).


O Que É um Dummy?

Um Dummy é simplesmente um objeto ou valor que é completamente irrelevante para o nosso caso de teste, mas que ainda assim somos obrigados a fornecer. Os dois motivos mais comuns pelos quais precisamos fornecer um dummy são:

  1. Nosso código não compilaria sem ele (por exemplo, um parâmetro obrigatório na assinatura de um método ou construtor).
  2. Nosso teste lançaria uma exceção indesejada porque um valor obrigatório está ausente (por exemplo, uma validação de pré-condição ou de Builder).

Vamos descobrir em quais situações práticas nos deparamos com esses problemas.


Quando Devemos Usar Dummies?

Como acabamos de ver, devemos usar dummies quando um objeto ou valor é irrelevante para o comportamento verificado pelo nosso caso de teste, e só precisamos dele para satisfazer o compilador ou passar por uma checagem de obrigatoriedade.

Vamos analisar dois exemplos concretos que demonstram essas situações.

1. Passando um Dummy como Parâmetro de Método

É bastante óbvio que, se o método testado recebe dois parâmetros obrigatórios, não podemos invocá-lo passando apenas um parâmetro, pois nosso código sequer compilará. Se apenas um dos parâmetros for relevante para o nosso caso de teste atual, podemos substituir o outro parâmetro por um dummy.

Suponha que estejamos escrevendo testes unitários para um método createCopyOf() na classe TaskService, que cria uma cópia de uma tarefa existente. Esse método recebe dois parâmetros:

  1. O ID da tarefa de origem (sourceTaskId: Long).
  2. Um objeto LoggedInUser (creator: LoggedInUser) que contém as informações do usuário autenticado que será registrado como criador da nova tarefa.

Além disso, esse método valida os argumentos recebidos e lança NotFoundException caso a tarefa de origem não seja encontrada no repositório. O código-fonte do Sistema Sob Teste fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.testwithspring.starter.testdoubles

class TaskService(private val taskRepository: TaskRepository) {

    fun createCopyOf(sourceTaskId: Long?, creator: LoggedInUser?): Task {
        requireNotNull(sourceTaskId) { "sourceTaskId cannot be null" }
        requireNotNull(creator) { "creator cannot be null" }

        val source = taskRepository.findById(sourceTaskId)
            ?: throw NotFoundException(sourceTaskId)

        // O restante da implementação deste método foi omitido
        return source
    }
}

Agora, imagine que queiramos escrever um teste unitário que garante que a exceção NotFoundException é lançada quando a tarefa de origem (sourceTaskId) não é encontrada no banco de dados.

Nesse cenário de teste específico, não nos importamos nem um pouco com o segundo parâmetro (creator: LoggedInUser), pois a execução falha antes mesmo de usar as informações do criador! No entanto, ainda precisamos passar algum objeto LoggedInUser válido como segundo argumento — caso contrário, o código não compila (ou falha na pré-condição requireNotNull(creator)). É exatamente aí que usamos um Dummy!

2. Usando um Dummy como Valor Obrigatório de um Objeto Criado

Quando criamos os dados de teste exigidos pelo nosso caso de teste, frequentemente precisamos instanciar objetos que possuem propriedades obrigatórias. Se essas propriedades não forem especificadas ou se um valor inválido (como string vazia) for utilizado, o construtor ou Builder lança uma exceção.

Por exemplo, a classe Task possui um Builder que garante que não podemos construir um objeto Task sem um creator e sem um title:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Builder {
    var id: Long? = null
    var assignee: Assignee? = null
    var creator: Creator? = null
    var description: String? = null
    var title: String? = null

    // Outros métodos omitidos para maior clareza

    fun build(): Task {
        val task = Task(this)

        checkCreator(task)
        checkTitle(task)

        return task
    }

    private fun checkCreator(task: Task) {
        requireNotNull(task.creator) { "Creator cannot be null" }
    }

    private fun checkTitle(task: Task) {
        val title = requireNotNull(task.title) { "Title cannot be null" }
        check(title.isNotEmpty()) { "Title cannot be empty" }
    }
}

Se quisermos criar um novo objeto Task em um teste onde apenas o status importa, temos que especificar manualmente os valores de creator e title ou criar um mecanismo que preencha esses campos obrigatórios com dummies por padrão — exatamente como já fizemos na aula Enfatizando o Estado Relevante do Objeto Criado!

Agora que sabemos quando usar dummies, vamos descobrir as melhores técnicas para criá-los.


Como Devemos Criar Dummies?

Criar dummies não é ciência de foguetes, mas as diferentes opções possuem nuances importantes para a legibilidade do teste. Vamos analisar quatro técnicas diferentes que podemos utilizar para criar dummies:

1. Usar um “Valor Insano” (Insane Value) para Tipos Simples

Já utilizamos essa técnica anteriormente quando enfatizamos o estado relevante do objeto criado.

A ideia dessa técnica é simplesmente usar um valor que deixe evidente para qualquer leitor que aquele dado não é utilizado pelo Sistema Sob Teste (por exemplo, ID_NOT_USED = -1L ou TITLE_NOT_IMPORTANT = "NOT_IMPORTANT"). Além disso, devemos sempre atribuir esse valor a uma constante ou variável local com nome descritivo para não espalhar números ou strings mágicas pelos testes.

Essa técnica é ideal quando precisamos fornecer um dummy para tipos primitivos, numéricos ou String.

2. Instanciar Diretamente com Construtor Padrão

Como um dummy é apenas um “preenchedor de espaço” (placeholder) exigido para o teste rodar, não precisamos configurar propriedades significativas nele. Por isso, instanciar a classe diretamente (LoggedInUser()) é uma solução simples, desde que a classe possua um construtor sem argumentos (ou com valores padrão) acessível.

Novamente, devemos sempre guardar o dummy em uma variável com nome explícito (como notImportant) antes de passá-lo para o Sistema Sob Teste, tornando o teste muito mais fácil de ler:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class TaskServiceTest {

    companion object {
        private const val SOURCE_TASK_ID = 3L
    }

    private lateinit var service: TaskService

    // Setup omitido para maior clareza

    @Test
    fun `createCopyOf deve lancar excecao quando tarefa nao for encontrada`() {
        val notImportant = LoggedInUser()
        service.createCopyOf(SOURCE_TASK_ID, notImportant)
    }
}

3. Usar um Framework de Mocking (MockK)

Usar um framework de mocking como o MockK é uma excelente escolha quando a classe do dummy não possui construtor vazio, exige múltiplos parâmetros complexos no construtor ou quando o parâmetro esperado é uma interface.

Mais uma vez, damos um nome revelador de intenção (notImportant) para a variável que recebe o dummy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import io.mockk.mockk

class TaskServiceTest {

    companion object {
        private const val SOURCE_TASK_ID = 3L
    }

    private lateinit var service: TaskService

    // Setup omitido para maior clareza

    @Test
    fun `createCopyOf deve lancar excecao quando tarefa nao for encontrada`() {
        val notImportant = mockk<LoggedInUser>(relaxed = true)
        service.createCopyOf(SOURCE_TASK_ID, notImportant)
    }
}

[!TIP] Vantagem do MockK sobre o Mockito clássico em Kotlin No Mockito antigo (Java), não era possível criar um dummy de uma classe final sem extensões extras. Como todas as classes e data classes em Kotlin são final por padrão, o MockK foi projetado nativamente para instrumentar classes finais do Kotlin sem nenhum problema! Falaremos em detalhes sobre isso no próximo tópico.

4. Usar um Método Fábrica Dedicado (dummyUser())

Se não pudermos (ou não quisermos) usar as técnicas anteriores para um objeto de valor, podemos criar nosso objeto dummy utilizando um método fábrica (factory method).

Se decidirmos usar essa técnica, não devemos reaproveitar métodos fábrica normais de cenários reais (como createAdminUser()), pois os nomes desses métodos e seus dados realistas enganam o leitor, dando a falsa impressão de que aquele objeto é relevante para o teste!

Em vez disso, devemos criar um método fábrica específico cujo nome deixe explícito que o objeto criado é apenas um dummy (ex.: LoggedInUserFactory.dummyUser()), preenchendo seus campos obrigatórios com valores dummy (“insanos”):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class TaskServiceTest {

    companion object {
        private const val SOURCE_TASK_ID = 3L
    }

    private lateinit var service: TaskService

    // Setup omitido para maior clareza

    @Test
    fun `createCopyOf deve lancar excecao quando tarefa nao for encontrada`() {
        val notImportant = LoggedInUserFactory.dummyUser()
        service.createCopyOf(SOURCE_TASK_ID, notImportant)
    }
}

Resumo

Nesta aula, aprendemos quatro pontos essenciais:

  • Um Dummy é um objeto ou valor completamente irrelevante para o caso de teste.
  • Fornecemos um Dummy ao Sistema Sob Teste apenas porque, sem ele, nosso código não compila ou falha em validações de obrigatoriedade.
  • Se o nosso dummy for um tipo primitivo/simples, devemos utilizar um “valor insano” armazenado em uma constante ou variável com nome explícito (NOT_IMPORTANT).
  • Se o nosso dummy for um objeto, podemos criá-lo instanciando seu construtor vazio, utilizando uma ferramenta de mocking (MockK) ou invocando um método fábrica dedicado (dummy...()).