Introdução ao Sistema Sob Teste (SUT)

Ao concluirmos esta aula, estaremos familiarizados com as classes que serão utilizadas nas demais aulas deste tópico para demonstrar diferentes técnicas de criação de dados de teste.

O objetivo desta aula não é criticar a forma como essas classes foram originalmente projetadas. Contudo, se você quiser estudar mais a fundo as decisões de design discutidas aqui (como o antipadrão de construtor telescópico e o padrão JavaBeans versus imutabilidade em Kotlin), recomendamos a leitura dos materiais complementares indicados nas caixas de nota ao longo da aula.

Introdução à Classe MailingAddress

A classe MailingAddress armazena as informações de um único endereço de correspondência. Como a nossa aplicação precisa lidar tanto com pedidos domésticos (enviados para a Finlândia) quanto com pedidos internacionais, a classe MailingAddress deve conter as seguintes informações:

  • O nome do destinatário (receiver)
  • Endereço de rua (street address)
  • Caixa postal (post office box)
  • Código postal / CEP (post code)
  • Cidade (city)
  • Estado (state)
  • País (country)

A estrutura inicial de propriedades da classe MailingAddress em Kotlin tem a seguinte aparência:

MailingAddress.kt linhas 3–11
Arquivo Real KOTLIN
 3
 4
 5
 6
 7
 8
 9
10
11
class MailingAddress {

    val receiver: String
    val streetAddress: String?
    val postOfficeBox: String?
    val postCode: String
    val city: String
    val state: String?
    val country: String?

Há três regras de domínio importantes que devemos observar sobre esses atributos:

  • Um endereço pode ter um endereço de rua (streetAddress) ou uma caixa postal (postOfficeBox).
  • A informação de estado (state) não é obrigatória em todos os países (como na Finlândia ou na Suécia). Entretanto, se quisermos entregar um pedido nos Estados Unidos, precisamos obrigatoriamente fornecer essa informação.
  • A informação de país (country) não é obrigatória se quisermos entregar pedidos em endereços domésticos (Finlândia).

Os desenvolvedores originais da nossa aplicação decidiram utilizar o (anti)padrão do construtor telescópico (telescoping constructor (anti)pattern) para criar novos objetos MailingAddress. Eles escreveram três construtores sobrecarregados, que são descritos passo a passo a seguir:

Primeiro, se quisermos entregar um pedido nos Estados Unidos, precisamos utilizar o construtor que nos permite definir todos os campos do objeto MailingAddress criado. O código-fonte da classe com este construtor completo tem a seguinte aparência:

MailingAddress.kt linhas 3–30
Arquivo Real KOTLIN
 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
28
29
class MailingAddress {

    val receiver: String
    val streetAddress: String?
    val postOfficeBox: String?
    val postCode: String
    val city: String
    val state: String?
    val country: String?

    constructor(
        receiver: String,
        streetAddress: String?,
        postOfficeBox: String?,
        postCode: String,
        city: String,
        state: String?,
        country: String?
    ) {
        this.receiver = receiver
        this.streetAddress = streetAddress
        this.postOfficeBox = postOfficeBox
        this.postCode = postCode
        this.city = city
        this.state = state
        this.country = country
    }

Podemos utilizar este construtor de 7 parâmetros para criar novos objetos MailingAddress que representam os seguintes endereços nos Estados Unidos (um com rua e outro com caixa postal):

1
2
3
4
5
6
7
8
9
Teddy Tester
Test Street 51
Test City TX 11111
USA

Teddy Tester
PO Box 1
Test City TX 11111
USA

Segundo, se quisermos entregar um pedido em um país estrangeiro que não exige a informação de estado (como a Suécia), podemos utilizar o construtor secundário de 6 parâmetros, que delega para o construtor principal passando null no lugar de state. O código-fonte deste construtor tem a seguinte aparência:

MailingAddress.kt linhas 3–46
Arquivo Real KOTLIN
 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class MailingAddress {

    val receiver: String
    val streetAddress: String?
    val postOfficeBox: String?
    val postCode: String
    val city: String
    val state: String?
    val country: String?

    constructor(
        receiver: String,
        streetAddress: String?,
        postOfficeBox: String?,
        postCode: String,
        city: String,
        state: String?,
        country: String?
    ) {
        this.receiver = receiver
        this.streetAddress = streetAddress
        this.postOfficeBox = postOfficeBox
        this.postCode = postCode
        this.city = city
        this.state = state
        this.country = country
    }

    constructor(
        receiver: String,
        streetAddress: String?,
        postOfficeBox: String?,
        postCode: String,
        city: String,
        country: String?
    ) : this(
        receiver = receiver,
        streetAddress = streetAddress,
        postOfficeBox = postOfficeBox,
        postCode = postCode,
        city = city,
        state = null,
        country = country
    )

Podemos utilizar este construtor para criar novos objetos MailingAddress que contêm os seguintes endereços na Suécia:

1
2
3
4
5
6
7
8
9
Teddy Tester
Test Street 51
11111 Test City
SWEDEN

Teddy Tester
PO Box 1
11111 Test City
SWEDEN

Terceiro, se quisermos entregar um pedido em um endereço doméstico na Finlândia, podemos utilizar o construtor secundário de 5 parâmetros, que delega para o construtor de 6 parâmetros passando null para o país (country). O código-fonte completo com o terceiro construtor tem a seguinte aparência:

MailingAddress.kt
Arquivo Real KOTLIN
 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.testwithspring.starter.testdata

class MailingAddress {

    val receiver: String
    val streetAddress: String?
    val postOfficeBox: String?
    val postCode: String
    val city: String
    val state: String?
    val country: String?

    constructor(
        receiver: String,
        streetAddress: String?,
        postOfficeBox: String?,
        postCode: String,
        city: String,
        state: String?,
        country: String?
    ) {
        this.receiver = receiver
        this.streetAddress = streetAddress
        this.postOfficeBox = postOfficeBox
        this.postCode = postCode
        this.city = city
        this.state = state
        this.country = country
    }

    constructor(
        receiver: String,
        streetAddress: String?,
        postOfficeBox: String?,
        postCode: String,
        city: String,
        country: String?
    ) : this(
        receiver = receiver,
        streetAddress = streetAddress,
        postOfficeBox = postOfficeBox,
        postCode = postCode,
        city = city,
        state = null,
        country = country
    )

    constructor(
        receiver: String,
        streetAddress: String?,
        postOfficeBox: String?,
        postCode: String,
        city: String
    ) : this(
        receiver = receiver,
        streetAddress = streetAddress,
        postOfficeBox = postOfficeBox,
        postCode = postCode,
        city = city,
        country = null
    )
}

Podemos utilizar este terceiro construtor para criar novos objetos MailingAddress que contêm os seguintes endereços domésticos na Finlândia:

1
2
3
4
5
6
7
Teddy Tester
Test Street 51
11111 Test City

Teddy Tester
PO Box 1
11111 Test City

Nota sobre Kotlin vs. Java Clássico:

Em Java, não havia suporte a valores padrão de parâmetros ou argumentos nomeados, o que levava desenvolvedores a encadear múltiplos construtores (this(...)) — o chamado construtor telescópico. Em Kotlin 2.4, mesmo quando interagimos com classes legadas ou modelos que possuem sobrecargas telescópicas (ou anotadas com @JvmOverloads), podemos declarar um construtor primário conciso e tirar proveito de argumentos nomeados, como exploraremos em detalhes na próxima aula.

Leitura Adicional:

A seguir, examinaremos em detalhes a segunda classe de domínio do nosso sistema sob teste: a classe Task.

Introdução à Classe Task

A classe Task é utilizada em um rastreador de tarefas simples (task tracker) e armazena as informações de uma única tarefa. Por esse motivo, a classe Task deve conter as seguintes informações:

  • Identificador (id)
  • O responsável pela tarefa (assignee — usuário a quem a tarefa foi atribuída)
  • O usuário que fechou a tarefa (closer)
  • O usuário que criou a tarefa (creator)
  • Descrição (description)
  • O usuário que modificou a tarefa (modifier)
  • Título (title)
  • Status da tarefa (status)
  • Resolução da tarefa (resolution)

A estrutura de atributos da classe Task tem a seguinte aparência:

Task.kt
Arquivo Real KOTLIN
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.testwithspring.starter.testdata

class Task() {

    var id: Long? = null
    var assignee: Assignee? = null
    var closer: Closer? = null
    var creator: Creator? = null
    var description: String? = null
    var modifier: Modifier? = null
    var title: String? = null
    var status: TaskStatus? = null
    var resolution: TaskResolution? = null
}

As classes Assignee, Closer, Creator e Modifier são objetos de valor (value objects) imutáveis que armazenam o userId do usuário correspondente em cada papel.

O código-fonte da classe Assignee tem a seguinte aparência:

Assignee.kt
Arquivo Real KOTLIN
1
2
3
4
5
package com.testwithspring.starter.testdata

data class Assignee(
    val userId: Long
)

O código-fonte da classe Closer tem a seguinte aparência:

Closer.kt
Arquivo Real KOTLIN
1
2
3
4
5
package com.testwithspring.starter.testdata

data class Closer(
    val userId: Long
)

O código-fonte da classe Creator tem a seguinte aparência:

Creator.kt
Arquivo Real KOTLIN
1
2
3
4
5
package com.testwithspring.starter.testdata

data class Creator(
    val userId: Long
)

O código-fonte da classe Modifier tem a seguinte aparência:

Modifier.kt
Arquivo Real KOTLIN
1
2
3
4
5
package com.testwithspring.starter.testdata

data class Modifier(
    val userId: Long
)

O enum TaskStatus especifica os possíveis status de uma tarefa. Seu código-fonte tem a seguinte aparência:

TaskStatus.kt
Arquivo Real KOTLIN
1
2
3
4
5
6
7
package com.testwithspring.starter.testdata

enum class TaskStatus {
    CLOSED,
    IN_PROGRESS,
    OPEN
}

O enum TaskResolution especifica as possíveis resoluções de uma tarefa quando ela é encerrada. Seu código-fonte tem a seguinte aparência:

TaskResolution.kt
Arquivo Real KOTLIN
1
2
3
4
5
6
7
package com.testwithspring.starter.testdata

enum class TaskResolution {
    DUPLICATE,
    DONE,
    WONT_DO
}

O diagrama de classes a seguir ilustra os relacionamentos entre todos esses componentes:

classDiagram class Task { +Long? id +Assignee? assignee +Closer? closer +Creator? creator +String? description +Modifier? modifier +String? title +TaskStatus? status +TaskResolution? resolution } class Assignee { +Long userId } class Closer { +Long userId } class Creator { +Long userId } class Modifier { +Long userId } class TaskStatus { <> CLOSED IN_PROGRESS OPEN } class TaskResolution { <> DUPLICATE DONE WONT_DO } Task --> Assignee : assignee Task --> Closer : closer Task --> Creator : creator Task --> Modifier : modifier Task --> TaskStatus : status Task --> TaskResolution : resolution

Os desenvolvedores da nossa aplicação de rastreamento de tarefas decidiram utilizar o padrão JavaBeans ao criar a classe Task. Isso significa que:

  • Podemos criar um novo objeto Task utilizando um construtor sem argumentos (Task()).
  • Podemos definir os valores das propriedades do objeto criado utilizando métodos setter (ou atribuição direta às propriedades var em Kotlin, que invocam os setters na JVM).
  • Podemos obter os valores das propriedades do objeto criado utilizando métodos getter (ou leitura direta das propriedades em Kotlin).

Em Java tradicional, a classe Task exigia dezenas de linhas de métodos get*() e set*() explícitos. Em Kotlin, propriedades mutáveis (var) geram automaticamente os getters e setters compatíveis com a especificação JavaBeans no bytecode da JVM, embora também possamos visualizar o equivalente completo com construtor vazio e métodos explícitos quando lidamos com classes legadas:

Task.kt
Arquivo Real KOTLIN
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.testwithspring.starter.testdata

class Task() {

    var id: Long? = null
    var assignee: Assignee? = null
    var closer: Closer? = null
    var creator: Creator? = null
    var description: String? = null
    var modifier: Modifier? = null
    var title: String? = null
    var status: TaskStatus? = null
    var resolution: TaskResolution? = null
}

Caso você esteja inspecionando uma classe Task vinda de código legado estilo JavaBeans com getters e setters explícitos traduzidos literalmente, ela expõe o seguinte contrato completo:

TaskJavaBeansLegacy.kt
Arquivo Real KOTLIN
 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.testwithspring.starter.testdata

class TaskJavaBeansLegacy {

    private var id: Long? = null
    private var assignee: Assignee? = null
    private var closer: Closer? = null
    private var creator: Creator? = null
    private var description: String? = null
    private var modifier: Modifier? = null
    private var title: String? = null
    private var status: TaskStatus? = null
    private var resolution: TaskResolution? = null

    constructor()

    fun getId(): Long? = id

    fun getAssignee(): Assignee? = assignee

    fun getCloser(): Closer? = closer

    fun getCreator(): Creator? = creator

    fun getDescription(): String? = description

    fun getModifier(): Modifier? = modifier

    fun getTitle(): String? = title

    fun getStatus(): TaskStatus? = status

    fun getResolution(): TaskResolution? = resolution

    fun setId(id: Long?) {
        this.id = id
    }

    fun setAssignee(assignee: Assignee?) {
        this.assignee = assignee
    }

    fun setCloser(closer: Closer?) {
        this.closer = closer
    }

    fun setCreator(creator: Creator?) {
        this.creator = creator
    }

    fun setDescription(description: String?) {
        this.description = description
    }

    fun setModifier(modifier: Modifier?) {
        this.modifier = modifier
    }

    fun setTitle(title: String?) {
        this.title = title
    }

    fun setStatus(status: TaskStatus?) {
        this.status = status
    }

    fun setResolution(resolution: TaskResolution?) {
        this.resolution = resolution
    }
}

Agora estamos familiarizados com as classes que serão utilizadas nas demais aulas deste tópico. Vamos resumir o que aprendemos nesta aula.

Evidência de Execução dos Modelos de Domínio

Para verificar que todas as classes do Sistema Sob Teste (SUT) e seus construtores funcionam corretamente no runtime do Kotlin e JUnit Jupiter 6, executamos o teste de sanidade:

SutSanityTest.kt
Arquivo Real KOTLIN
 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.testwithspring.starter.testdata

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

@DisplayName("SUT Sanity - Verificação do Modelo de Domínio")
class SutSanityTest {

    @Test
    @DisplayName("Deve criar MailingAddress com construtor de 7 parâmetros para endereço dos EUA")
    fun `deve instanciar endereco dos EUA com 7 parametros`() {
        val address = MailingAddress(
            receiver = "Teddy Tester",
            streetAddress = "Test Street 51",
            postOfficeBox = null,
            postCode = "11111",
            city = "Test City",
            state = "TX",
            country = "USA"
        )

        assertThat(address.receiver).isEqualTo("Teddy Tester")
        assertThat(address.streetAddress).isEqualTo("Test Street 51")
        assertThat(address.postOfficeBox).isNull()
        assertThat(address.postCode).isEqualTo("11111")
        assertThat(address.city).isEqualTo("Test City")
        assertThat(address.state).isEqualTo("TX")
        assertThat(address.country).isEqualTo("USA")
    }

    @Test
    @DisplayName("Deve criar MailingAddress com construtor de 6 parâmetros para Suécia (sem state)")
    fun `deve instanciar endereco sueco com 6 parametros`() {
        val address = MailingAddress(
            receiver = "Teddy Tester",
            streetAddress = "Test Street 51",
            postOfficeBox = null,
            postCode = "11111",
            city = "Test City",
            country = "SWEDEN"
        )

        assertThat(address.state).isNull()
        assertThat(address.country).isEqualTo("SWEDEN")
    }

    @Test
    @DisplayName("Deve criar MailingAddress com construtor de 5 parâmetros para Finlândia (sem state e sem country)")
    fun `deve instanciar endereco finlandes com 5 parametros`() {
        val address = MailingAddress(
            receiver = "Teddy Tester",
            streetAddress = "Test Street 51",
            postOfficeBox = null,
            postCode = "11111",
            city = "Test City"
        )

        assertThat(address.state).isNull()
        assertThat(address.country).isNull()
    }

    @Test
    @DisplayName("Deve criar e configurar objeto Task estilo JavaBeans")
    fun `deve criar e configurar objeto Task JavaBeans`() {
        val task = Task().apply {
            id = 1L
            title = "Implementar SUT"
            description = "Configurar modelos de dominio para testes"
            status = TaskStatus.OPEN
            creator = Creator(10L)
            assignee = Assignee(20L)
        }

        assertThat(task.id).isEqualTo(1L)
        assertThat(task.title).isEqualTo("Implementar SUT")
        assertThat(task.status).isEqualTo(TaskStatus.OPEN)
        assertThat(task.creator?.userId).isEqualTo(10L)
        assertThat(task.assignee?.userId).isEqualTo(20L)
        assertThat(task.closer).isNull()
        assertThat(task.resolution).isNull()
    }
}
Evidência Real de Execução: Verificação do SUT (./gradlew test)
Terminal Log
Initialized native services in: /home/wc/.gradle/native
Initialized jansi services in: /home/wc/.gradle/native
Gradle Daemon started in 1 s 105 ms
Starting Build
Settings evaluated using settings file '/home/wc/test-with-spring/code/01-starter-junit6/settings.gradle.kts'.
Projects loaded. Root project using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Included projects: [root project 'starter-junit6']

> Configure project :
Evaluating root project 'starter-junit6' using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Using Kotlin Gradle Plugin 2.4.21
Using Java Toolchain: Temurin 21.0.6 (/home/wc/.sdkman/candidates/java/21.0.6-tem)
Spring Boot 4.1.2 plugin applied.
All projects evaluated.
Selected primary task 'test' from project :
Tasks to be executed: [task ':compileKotlin', task ':compileJava', task ':processResources', task ':classes', task ':compileTestKotlin', task ':compileTestJava', task ':processTestResources', task ':testClasses', task ':test']
Tasks that were skipped: [task ':compileJava', task ':compileTestJava']

> Task :compileKotlin
Compiling with Kotlin compiler version 2.4.21 (JVM target 21)
Compiling Kotlin files from [/home/wc/test-with-spring/code/01-starter-junit6/src/main/kotlin]

> Task :processResources NO-SOURCE
> Task :classes
> Task :compileTestKotlin
Compiling with Kotlin compiler version 2.4.21 (JVM target 21)
Compiling Kotlin files from [/home/wc/test-with-spring/code/01-starter-junit6/src/test/kotlin]

> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
Starting process 'Gradle Test Executor 1'. Working directory: /home/wc/test-with-spring/code/01-starter-junit6 Command: /home/wc/.sdkman/candidates/java/21.0.6-tem/bin/java -ea -cp ...
Successfully started process 'Gradle Test Executor 1'

com.testwithspring.starter.testdata.SutSanityTest > Deve criar MailingAddress com construtor de 7 parâmetros para endereço dos EUA PASSED
com.testwithspring.starter.testdata.SutSanityTest > Deve criar MailingAddress com construtor de 6 parâmetros para Suécia (sem state) PASSED
com.testwithspring.starter.testdata.SutSanityTest > Deve criar MailingAddress com construtor de 5 parâmetros para Finlândia (sem state e sem country) PASSED
com.testwithspring.starter.testdata.SutSanityTest > Deve criar e configurar objeto Task estilo JavaBeans PASSED

SUT Sanity - Verificação do Modelo de Domínio > 4 tests completed, 0 failed, 0 skipped
Finished generating test XML results into: /home/wc/test-with-spring/code/01-starter-junit6/build/test-results/test
Finished generating test html results into: /home/wc/test-with-spring/code/01-starter-junit6/build/reports/tests/test

BUILD SUCCESSFUL in 2s
5 actionable tasks: 5 executed

Resumo

Esta aula nos ensinou quatro pontos principais:

  • A classe MailingAddress armazena as informações de um único endereço de correspondência (doméstico ou internacional).
  • Podemos criar novos objetos MailingAddress utilizando o (anti)padrão do construtor telescópico.
  • A classe Task armazena as informações de uma única tarefa no nosso sistema de acompanhamento de tarefas, relacionando-se com os objetos de valor Assignee, Closer, Creator e Modifier e com os enums TaskStatus e TaskResolution.
  • A classe Task utiliza o padrão de construção estilo JavaBeans (construtor sem argumentos e mutação de estado por propriedades/setters).