ArchUnit: Garantindo a Integridade Arquitetural de Aplicações Java
Índice Introdução O que o ArchUnit faz? Por que é importante? Documentação Executável Feedback Rápido Refatoração Segura Padronização em Equipes Distribuídas Evolução Controlada Começando com ArchUnit Configuração do Projeto Escrevendo seu Primeiro Teste Executando os Testes Exemplos Práticos Arquitetura em Camadas Arquitetura Limpa Recursos Avançados do ArchUnit Regras Personalizadas Complexas Verificação de Interfaces e Herança Detecção de Ciclos de Dependência Cache de Importação Mensagens de Erro Personalizadas Utilizando Regras Pré-definidas Verificando Convenções de Nomenclatura Congelamento de Regras para Projetos Legados Conclusão Referência Bibliográfica Introdução Em projetos de software de médio e grande porte, garantir que o código siga os padrões arquiteturais definidos é um desafio contínuo. Conforme o código cresce e mais desenvolvedores participam do projeto, torna-se cada vez mais difícil manter a consistência arquitetural. É nesse contexto que o ArchUnit surge como uma ferramenta poderosa para desenvolvedores Java. ArchUnit é uma biblioteca Java de código aberto que possibilita testar de forma automatizada se o código segue as regras arquiteturais estabelecidas. Ao contrário de ferramentas de análise estática tradicionais, o ArchUnit permite que você defina regras específicas para seu domínio e integre os testes de arquitetura diretamente na sua suíte de testes unitários. O que o ArchUnit faz? O ArchUnit permite que você teste aspectos como: Dependências entre pacotes e classes: Garantir que camadas específicas não dependam de outras camadas indevidamente. Convenções de nomenclatura: Certificar que nomes de classes e métodos sigam os padrões estabelecidos. Uso adequado de anotações: Verificar se anotações específicas estão presentes onde deveriam estar. Acesso a métodos e campos: Verificar que determinados métodos ou campos só são acessados em contextos específicos. Estrutura de herança: Garantir que certas classes herdem ou implementem interfaces específicas. Ciclos de dependência: Identificar e evitar ciclos de dependência que possam comprometer a manutenibilidade. Validação de padrões arquiteturais conhecidos: Testar a conformidade com padrões como Arquitetura em Camadas, Arquitetura Limpa, etc. Por que é importante? A importância do ArchUnit vai além da simples verificação de código: 1. Documentação Executável O ArchUnit transforma decisões arquiteturais em testes executáveis, servindo como documentação viva do projeto. Novos membros da equipe podem entender rapidamente as convenções arquiteturais examinando os testes. 2. Feedback Rápido Ao integrar testes de arquitetura na sua pipeline de CI/CD, você recebe feedback imediato quando uma mudança no código viola as regras arquiteturais estabelecidas, prevenindo a degradação gradual da arquitetura. 3. Refatoração Segura Durante grandes refatorações, o ArchUnit pode garantir que as mudanças respeitem as regras arquiteturais, dando confiança para evoluir o código com segurança. 4. Padronização em Equipes Distribuídas Em equipes grandes ou distribuídas, o ArchUnit ajuda a manter consistência nas práticas de desenvolvimento, mesmo quando a comunicação direta entre equipes é limitada. 5. Evolução Controlada Quando a arquitetura precisa evoluir, você pode atualizar os testes de arquitetura para refletir as novas diretrizes, facilitando a transição controlada de um padrão arquitetural para outro. Começando com ArchUnit Vamos ver como configurar e começar a usar o ArchUnit em um projeto Java: Configuração do Projeto Para Maven (pom.xml): com.tngtech.archunit archunit 1.2.1 test com.tngtech.archunit archunit-junit5 1.2.1 test Para Gradle (build.gradle): dependencies { testImplementation 'com.tngtech.archunit:archunit:1.2.1' testImplementation 'com.tngtech.archunit:archunit-junit5:1.2.1' } Escrevendo seu Primeiro Teste Depois de adicionar as dependências, você pode criar seu primeiro teste de arquitetura. Comece com algo simples: package com.example.archunit; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.lang.ArchRule; import org.junit.jupiter.api.Test; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; public class ArchitectureTest { // Importando classes do seu projeto para análise JavaClasses importedClasses = new ClassFileImporter() .importPackages("com.example.project"); @Test public void servicesShouldNotDependOnControllers() { // Definindo uma regra arquitetural ArchRule rule = classes() .that().resideInAPackage("..service..") .should().notDependOnClassesThat() .reside

Índice
- Introdução
- O que o ArchUnit faz?
-
Por que é importante?
- Documentação Executável
- Feedback Rápido
- Refatoração Segura
- Padronização em Equipes Distribuídas
- Evolução Controlada
-
Começando com ArchUnit
- Configuração do Projeto
- Escrevendo seu Primeiro Teste
- Executando os Testes
-
Exemplos Práticos
- Arquitetura em Camadas
- Arquitetura Limpa
-
Recursos Avançados do ArchUnit
- Regras Personalizadas Complexas
- Verificação de Interfaces e Herança
- Detecção de Ciclos de Dependência
- Cache de Importação
- Mensagens de Erro Personalizadas
- Utilizando Regras Pré-definidas
- Verificando Convenções de Nomenclatura
- Congelamento de Regras para Projetos Legados
- Conclusão
- Referência Bibliográfica
Introdução
Em projetos de software de médio e grande porte, garantir que o código siga os padrões arquiteturais definidos é um desafio contínuo. Conforme o código cresce e mais desenvolvedores participam do projeto, torna-se cada vez mais difícil manter a consistência arquitetural. É nesse contexto que o ArchUnit surge como uma ferramenta poderosa para desenvolvedores Java.
ArchUnit é uma biblioteca Java de código aberto que possibilita testar de forma automatizada se o código segue as regras arquiteturais estabelecidas. Ao contrário de ferramentas de análise estática tradicionais, o ArchUnit permite que você defina regras específicas para seu domínio e integre os testes de arquitetura diretamente na sua suíte de testes unitários.
O que o ArchUnit faz?
O ArchUnit permite que você teste aspectos como:
- Dependências entre pacotes e classes: Garantir que camadas específicas não dependam de outras camadas indevidamente.
- Convenções de nomenclatura: Certificar que nomes de classes e métodos sigam os padrões estabelecidos.
- Uso adequado de anotações: Verificar se anotações específicas estão presentes onde deveriam estar.
- Acesso a métodos e campos: Verificar que determinados métodos ou campos só são acessados em contextos específicos.
- Estrutura de herança: Garantir que certas classes herdem ou implementem interfaces específicas.
- Ciclos de dependência: Identificar e evitar ciclos de dependência que possam comprometer a manutenibilidade.
- Validação de padrões arquiteturais conhecidos: Testar a conformidade com padrões como Arquitetura em Camadas, Arquitetura Limpa, etc.
Por que é importante?
A importância do ArchUnit vai além da simples verificação de código:
1. Documentação Executável
O ArchUnit transforma decisões arquiteturais em testes executáveis, servindo como documentação viva do projeto. Novos membros da equipe podem entender rapidamente as convenções arquiteturais examinando os testes.
2. Feedback Rápido
Ao integrar testes de arquitetura na sua pipeline de CI/CD, você recebe feedback imediato quando uma mudança no código viola as regras arquiteturais estabelecidas, prevenindo a degradação gradual da arquitetura.
3. Refatoração Segura
Durante grandes refatorações, o ArchUnit pode garantir que as mudanças respeitem as regras arquiteturais, dando confiança para evoluir o código com segurança.
4. Padronização em Equipes Distribuídas
Em equipes grandes ou distribuídas, o ArchUnit ajuda a manter consistência nas práticas de desenvolvimento, mesmo quando a comunicação direta entre equipes é limitada.
5. Evolução Controlada
Quando a arquitetura precisa evoluir, você pode atualizar os testes de arquitetura para refletir as novas diretrizes, facilitando a transição controlada de um padrão arquitetural para outro.
Começando com ArchUnit
Vamos ver como configurar e começar a usar o ArchUnit em um projeto Java:
Configuração do Projeto
Para Maven (pom.xml):
com.tngtech.archunit
archunit
1.2.1
test
com.tngtech.archunit
archunit-junit5
1.2.1
test
Para Gradle (build.gradle):
dependencies {
testImplementation 'com.tngtech.archunit:archunit:1.2.1'
testImplementation 'com.tngtech.archunit:archunit-junit5:1.2.1'
}
Escrevendo seu Primeiro Teste
Depois de adicionar as dependências, você pode criar seu primeiro teste de arquitetura. Comece com algo simples:
package com.example.archunit;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
public class ArchitectureTest {
// Importando classes do seu projeto para análise
JavaClasses importedClasses = new ClassFileImporter()
.importPackages("com.example.project");
@Test
public void servicesShouldNotDependOnControllers() {
// Definindo uma regra arquitetural
ArchRule rule = classes()
.that().resideInAPackage("..service..")
.should().notDependOnClassesThat()
.resideInAPackage("..controller..");
// Verificando se a regra é respeitada
rule.check(importedClasses);
}
}
Este teste simples verifica se as classes no pacote service
não dependem de classes no pacote controller
, o que é uma regra comum em arquiteturas em camadas.
Executando os Testes
Para executar os testes de arquitetura:
- Via Maven:
mvn test
- Via Gradle:
./gradlew test
- Na sua IDE: Execute o teste como qualquer outro teste JUnit.
Se alguma regra for violada, você verá um erro no teste com uma mensagem detalhada explicando quais classes violaram a regra e como.
Por exemplo:
Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in a package '..service..' should not depend on classes that reside in a package '..controller..'' was violated (1 times):
Method com.example.project.service.UserService.getControllerData() calls method com.example.project.controller.UserController.getData()
Utilizando JUnit 5 para Simplificar os Testes
Se você estiver usando JUnit 5, pode simplificar ainda mais seus testes usando as anotações fornecidas pelo ArchUnit:
@AnalyzeClasses(packages = "com.example.project")
public class ArchitectureRulesTest {
// O ArchUnit irá importar automaticamente as classes e verificar a regra
@ArchTest
static final ArchRule servicesShouldNotDependOnControllers = classes()
.that().resideInAPackage("..service..")
.should().notDependOnClassesThat()
.resideInAPackage("..controller..");
// Regras adicionais podem ser definidas como campos estáticos
@ArchTest
static final ArchRule repositoriesShouldBeAccessedOnlyByServices = classes()
.that().resideInAPackage("..repository..")
.should().onlyBeAccessedByClassesThat()
.resideInAPackage("..service..");
}
Usando esta abordagem, não é necessário escrever explicitamente o código para importar as classes ou verificar as regras. O ArchUnit cuida disso automaticamente e também implementa um cache de classes entre os testes para melhorar o desempenho.
Exemplos Práticos
Vejamos dois exemplos práticos de como utilizar o ArchUnit para garantir a integridade de diferentes padrões arquiteturais:
Exemplo 1: Arquitetura em Camadas (Layered Architecture)
Este exemplo demonstra como validar uma arquitetura em camadas tradicional, comum em muitas aplicações:
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.library.Architectures;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
public class LayeredArchitectureTest {
@Test
public void testLayeredArchitecture() {
JavaClasses importedClasses = new ClassFileImporter()
.importPackages("com.mycompany.app");
// Definindo a arquitetura em camadas usando o padrão predefinido
Architectures.LayeredArchitecture layeredArchitecture = layeredArchitecture()
// Definindo as camadas e os pacotes onde elas residem
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..repository..")
.layer("Domain").definedBy("..domain..")
// Definindo as regras de dependência entre camadas
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service")
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Service", "Repository");
// Verificando a arquitetura
layeredArchitecture.check(importedClasses);
}
}
Este teste valida um padrão arquitetural em camadas típico onde:
- Controllers ficam no topo da hierarquia e não devem ser acessados por outras camadas
- Services podem ser acessados apenas pelos Controllers
- Repositories podem ser acessados apenas por Services
- O Domain (entidades) pode ser acessado por Services e Repositories
Exemplo 2: Arquitetura Limpa (Clean Architecture / Onion Architecture)
Este exemplo verifica a conformidade com os princípios da Arquitetura Limpa:
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
public class CleanArchitectureTest {
@Test
public void testCleanArchitecture() {
JavaClasses importedClasses = new ClassFileImporter()
.importPackages("com.mycompany.app");
// Definindo a arquitetura limpa (onion architecture)
// Note que esta funcionalidade está disponível no pacote com.tngtech.archunit.library
onionArchitecture()
// Definindo as camadas da arquitetura limpa
.domainModels("..domain.model..")
.domainServices("..domain.service..")
.applicationServices("..application..")
.adapter("persistence", "..adapter.persistence..")
.adapter("rest", "..adapter.rest..")
.adapter("cli", "..adapter.cli..")
// A verificação é implícita: camadas internas não podem
// depender de camadas externas
.check(importedClasses);
}
}
Este teste valida os princípios fundamentais da Arquitetura Limpa, onde:
- O núcleo do domínio não depende de nenhuma outra camada
- A camada de aplicação depende apenas do domínio
- Os adaptadores (persistence, rest, cli) dependem da aplicação e do domínio
Note que a função onionArchitecture()
é disponibilizada pelo pacote com.tngtech.archunit.library.Architectures
, então certifique-se de incluir a dependência correta.
Recursos Avançados do ArchUnit
Além dos exemplos acima, o ArchUnit oferece recursos avançados que muitos desenvolvedores desconhecem:
Regras Personalizadas Complexas
Você pode criar regras de arquitetura altamente específicas usando a API fluente do ArchUnit:
ArchRule rule = classes()
.that().areAnnotatedWith(RestController.class)
.should().haveSimpleNameEndingWith("Controller")
.andShould().bePublic()
.andShould().onlyAccessMethodsThat(areDeclaredInController().or(areDeclaredInService()));
Verificação de Interfaces e Herança
Embora o ArchUnit não tenha suporte nativo direto para verificação de hierarquia completa, você pode verificar implementações de interfaces ou extensões de classes específicas:
// Verificando se todas as classes em um pacote implementam uma interface
ArchRule rule = classes()
.that().resideInAPackage("..repository..")
.should().implement(JpaRepository.class);
// Verificando se todas as classes que têm um determinado sufixo estendem uma classe base
ArchRule rule2 = classes()
.that().haveSimpleNameEndingWith("Controller")
.should().beAssignableTo(BaseController.class);
Detecção de Ciclos de Dependência
Os ciclos de dependência são um dos problemas arquiteturais mais difíceis de detectar manualmente:
ArchRule noCyclicDependencies = slices()
.matching("com.mycompany.app.(*)..")
.should().beFreeOfCycles();
Cache de Importação
Para melhorar a performance dos testes em bases de código grandes, o ArchUnit oferece opções de cache:
ClassFileImporter importer = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS);
JavaClasses classes = importer.importPackages("com.mycompany.app");
É importante notar que este cache funciona apenas durante a mesma execução de testes e não persiste entre diferentes execuções ou builds. Ele melhora a performance ao reutilizar as classes importadas na mesma execução dos testes.
Mensagens de Erro Personalizadas
Melhore a compreensão quando os testes falham:
ArchRule rule = classes()
.that().areAnnotatedWith(Entity.class)
.should().resideInAPackage("..domain.model..")
.because("Entities são parte do modelo de domínio");
Utilizando Regras Pré-definidas
O ArchUnit fornece várias regras pré-definidas na classe GeneralCodingRules
que podem ser utilizadas diretamente:
import static com.tngtech.archunit.library.GeneralCodingRules.*;
@ArchTest
static final ArchRule noClassesShouldUseJodaTime = NO_CLASSES_SHOULD_USE_JODATIME;
@ArchTest
static final ArchRule noClassesShouldUseStandardStreams = NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS;
@ArchTest
static final ArchRule noClassesShouldThrowGenericExceptions = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;
@ArchTest
static final ArchRule noClassesShouldUseJavaUtilLogging = NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING;
Observe que estas regras pré-definidas estão disponíveis no pacote com.tngtech.archunit.library
, e não no core do ArchUnit. Certifique-se de incluir a dependência correta.
Verificando Convenções de Nomenclatura
Você pode garantir que suas convenções de nomenclatura sejam seguidas:
@ArchTest
static final ArchRule serviceClassesShouldHaveServiceSuffix = classes()
.that().resideInAPackage("..service..")
.should().haveSimpleNameEndingWith("Service");
@ArchTest
static final ArchRule repositoryClassesShouldHaveRepositorySuffix = classes()
.that().resideInAPackage("..repository..")
.should().haveSimpleNameEndingWith("Repository")
.orShould().haveSimpleNameEndingWith("Dao");
Congelamento de Regras para Projetos Legados
Para equipes que trabalham com código legado, o ArchUnit oferece a funcionalidade de "congelamento" (freezing) de regras. Isso permite que você mantenha as violações existentes, mas impede novas violações:
// Criando uma regra normal
ArchRule rule = classes()
.that().resideInAPackage("..service..")
.should().notDependOnClassesThat()
.resideInAPackage("..controller..");
// Congelando a regra para permitir violações existentes
FreezingArchRule frozenRule = FreezingArchRule.freeze(rule);
// Verificando apenas novas violações
frozenRule.check(importedClasses);
Esta técnica é extremamente útil quando você quer melhorar gradualmente a arquitetura de um sistema legado sem ter que corrigir todas as violações de uma só vez.
Conclusão
O ArchUnit representa um avanço significativo na forma como garantimos a qualidade arquitetural em projetos Java. Ao transformar regras arquiteturais em testes automatizados, ele ajuda a manter a integridade do código mesmo à medida que o projeto cresce e evolui.
A ferramenta não apenas detecta violações arquiteturais precocemente, mas também serve como documentação viva das decisões arquiteturais tomadas pela equipe. Isto facilita a adaptação de novos membros e garante consistência em equipes distribuídas.
Se você está enfrentando desafios para manter a consistência arquitetural em seu projeto Java, o ArchUnit pode ser uma adição valiosa ao seu conjunto de ferramentas de qualidade de código.
Para ver um exemplo prático de implementação dos conceitos apresentados neste artigo, você pode acessar o repositório Arquitetura em Camadas com ArchUnit que demonstra a aplicação desses princípios com exemplos de código e testes de arquitetura personalizados.