Ir para o conteúdo principal
Background Image

Documentação Executável: Como transformar testes unitários em ferramenta de comunicação de design

··5 minutos·
Rafael Issao
Autor
Rafael Issao
Apaixonado por tecnologia, programação e inovação. Adoro compartilhar conhecimento e aprender coisas novas todos os dias.
Tabela de conteúdos

No início de qualquer projeto, tudo é bem feito.

Pensamos na arquitetura, organização das pastas, responsabilidade de cada função ou classe e tudo vai bem.

Até que, depois de um tempo, já está uma bagunça.

Temos métodos longos, temos dependências malucas e acoplamento tão forte que qualquer mudança pode causar um problema grande.

Neste artigo, trago uma dica rápida para você cuidar do seu design de uma forma metódica, sem depender da boa vontade das pessoas:

Documentação executável!

Documentação executável sobre decisão de design
#

Um dos atributos importantes de um bom design é quando encapsulamos a estrutura interna de um componente e criamos uma API clara de como podemos usar este componente.

Um exemplo de um design onde escondemos o detalhe da implementação:

Intenção do design
Intenção do design

A classe Patient_ é a API do pacote. Ou seja, tudo que quisermos fazer com o modelo Paciente, sempre devemos fazer através do objeto **_Patient_**.

Para representar o estado do paciente utilizamos o enum ManchesterProtocolClassification.

Mas não queremos que este enum seja utilizado fora deste pacote, exatamente para evitar códigos como o exemplo a seguir:

        List<String> lista = new ArrayList<>();
        lista.add("a");
        lista.add("b");
        lista.add("c");

        // ...

Então para evitar o uso indiscriminado do enum fora do pacote, criamos ele como package-private:

package br.com.youready.pap.patient.model;

import java.time.Duration;

enum ManchesterProtocolClassification {
    IMMEDIATE(Duration.ofMinutes(0)),
    VERY_URGENT(Duration.ofMinutes(10)),
    URGENT(Duration.ofMinutes(60)),
    STANDARD(Duration.ofHours(2)),
    NON_URGENT(Duration.ofHours(4));

    private final Duration maxWaitTime;

    ManchesterProtocolClassification(Duration maxWaitTime) {
        this.maxWaitTime = maxWaitTime;
    }

    public boolean isWaitingTimeExceed(Duration waitingTime) {
        return waitingTime.compareTo(maxWaitTime) > 0;
    }
}

E para criar o objeto Patient sem vazar o enum fora do pacote, podemos fazer o seguinte:

package br.com.youready.pap.patient.model;

import java.time.Duration;
import java.time.Instant;
import lombok.Getter;

/**
 * API do pacote br.com.youready.pap.patient.model
 */
public class Patient {
    /**
     * Único getter necessário para API
     */
    @Getter
    private final String name;

    /**
     * Campos que são detalhes de implementação. Não devemos criar getter para eles.
     */
    private final ManchesterProtocolClassification classification;

    private final Instant arrivalTime;

    /**
     * Construtor privado para evitar o vazamento do detalhe de implementação.
     * Neste caso, o enum ManchesterProtocolClassification é detalhe de implementação
     * que não deve vazar deste pacote
     */
    private Patient(String name, ManchesterProtocolClassification classification) {
        this.name = name;
        this.classification = classification;
        this.arrivalTime = Instant.now();
    }

    /**
     * Métodos de fábrica para cada estado possível do paciente.
     * Com isso:
     * - Evitamos paciente com classification com valor nulo.
     * - Deixamos claro, via API, que tipo de pacientes é possível criar.
     */
    public static Patient nonUrgent(String name) {
        return new Patient(name, ManchesterProtocolClassification.NON_URGENT);
    }

    public static Patient standard(String name) {
        return new Patient(name, ManchesterProtocolClassification.STANDARD);
    }

    public static Patient urgent(String name) {
        return new Patient(name, ManchesterProtocolClassification.URGENT);
    }

    public static Patient veryUrgent(String name) {
        return new Patient(name, ManchesterProtocolClassification.VERY_URGENT);
    }

    public static Patient immediate(String name) {
        return new Patient(name, ManchesterProtocolClassification.IMMEDIATE);
    }

    public boolean isWaitingTimeExceeded() {
        Duration waitingTime = Duration.between(this.arrivalTime, Instant.now());
        return this.classification.isWaitingTimeExceed(waitingTime);
    }
}

Com este design, fica fácil de refatorar e deixamos claro para outros desenvolvedores o que é possível fazer com o modelo definido.

Mas mesmo assim, é muito difícil de comunicar a decisão do seu design para a equipe e futuros profissionais que podem entrar na sua equipe.

Para isso, precisamos documentar. Mas não pode ser qualquer documentação. (Pois tem tanta documentação mal feita na área de desenvolvimento de software que treinamos as pessoas a não ler.)

Para resolver o problema de comunicação, podemos criar uma documentação executável da decisão do design:

import com.tngtech.archunit.core.domain.JavaClasses
import com.tngtech.archunit.core.importer.ClassFileImporter
import com.tngtech.archunit.lang.ArchRule
import spock.lang.Specification

/**
 * Utilizando Spock Framework e ArchUnit.
 * Exemplo simplificado de documentação executável
 */
class PatientModelDesignEncapsulationTest extends Specification {

    final String packageName = "br.com.youready.pap.patient.model"
    JavaClasses importedClasses;

    def setup() {
        importedClasses = new ClassFileImporter().withImportOption(DO_NOT_INCLUDE_TESTS)
                                                 .importPackages(packageName)
    }

    /**
     * Neste teste definimos a classe responsável por apresentar a API do pacote
     */
    def "Patient is the entity (or API) of the model. The reason for this is bla."() {
        when:
        ArchRule rule = ArchRuleDefinition.classes()
                                          .that()
                                          .haveSimpleName(Patient.simpleName)
                                          .should()
                                          .bePublic()

        then:
        rule.check(importedClasses)
    }

    def "All other classes must be package protected. Please hide the implementation details"() {
        when:
        ArchRule rule = ArchRuleDefinition.classes()
                .that()
                .doNotHaveSimpleName(Patient.simpleName)
                .should()
                .bePackagePrivate()

        then:
        rule.check(importedClasses)
    }
}

Podemos aproveitar a natureza dos testes unitários para documentar a decisão de design do código.

Com este teste, se uma pessoa nova entrar na equipe e deixar alguma classe pública além do Patient, o teste vai falhar.

É possível verificar em nível mais amplo também. Por exemplo, se seu projeto utiliza a “Onion Architecture”, poderia ter algo assim:

package br.com.youready.article.d_2024_11_25.image6;

import static com.tngtech.archunit.library.Architectures.onionArchitecture;

import com.tngtech.archunit.lang.ArchRule;

public class OnionArchitectureConfiguration {

    public ArchRule getOnionArchitectureRule() {
        return onionArchitecture()
                .domainModels("com.myapp.domain.model..")
                .domainServices("com.myapp.domain.service..")
                .applicationServices("com.myapp.application..")
                .adapter("cli", "com.myapp.adapter.cli..")
                .adapter("persistence", "com.myapp.adapter.persistence..")
                .adapter("rest", "com.myapp.adapter.rest..");
    }
}

É importante ter ferramentas de monitoramento para decisões de design e arquitetura para manter o conhecimento sempre atualizado e facilitar a propagação do mesmo!

Os exemplos estão em Java + Groovy e as ferramentas utilizadas foram:

🔗 Spock Framework

🔗 ArchUnit

A mesma ferramenta para outras linguagens:

🔗 PyTestArch para Python

🔗 ArchUnitNet para C#

🔗TS-Arch para Typescript/Javascript

Estas ferramentas fazem muito mais coisas, então vale a pena conferir!

Conclusão
#

Qualidade do código não é só sobre como escrevemos o código, mas também como organizamos ela dentro do projeto.

Este tipo de organização é um conhecimento que pode ser verificada de forma automatizada.

Podemos aproveitar a natureza do testes unitários para servir como uma documentação que é possível executá-la para verificar se a decisão tomada ainda é respeitada.

Bora começar a escrever mais testes interessantes e que trazem valor para a equipe e a empresa!

Se você tiver dúvidas, sugestões ou até correções, sinta-se à vontade para comentar ou falar diretamente comigo!

🥒🥒 Até o próximo artigo! 🥒🥒