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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.exemplo.endercos

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:

 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
package com.exemplo.enderecos

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:

 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
package com.exemplo.enderecos

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:

 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.exemplo.enderecos

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.exemplo.tarefas

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:

1
2
3
4
5
package com.exemplo.tarefas

data class Assignee(
    val userId: Long
)

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

1
2
3
4
5
package com.exemplo.tarefas

data class Closer(
    val userId: Long
)

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

1
2
3
4
5
package com.exemplo.tarefas

data class Creator(
    val userId: Long
)

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

1
2
3
4
5
package com.exemplo.tarefas

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:

1
2
3
4
5
6
7
package com.exemplo.tarefas

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:

1
2
3
4
5
6
7
package com.exemplo.tarefas

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.exemplo.tarefas

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:

 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.exemplo.tarefas

class Task {

    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.

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).