Bem-vindo ao blog de Felix Ricardo Gilioli

Compartilhando conhecimento sobre tecnologia, programação e engenharia de software

Testes Arquiteturais e ArchUnit

| Categorias: Java

Durante o desenvolvimento de um software, costumamos definir algumas convenções que os desenvolvedores devem seguir para que o projeto fique mais organizado, como por exemplo, classes de entidade devem ficar em um pacote específico, toda classe que contém determinada anotação deve ter um sufixo X, etc.

Porém mesmo definindo um padrão, é muito provável que em algum momento esqueçamos de algo, principalmente quando falamos de integrantes novos na equipe. Para resolver esse problema e ter uma garantia de qualidade a mais, podemos criar testes arquiteturais. Os testes de arquitetura além de trazer uma garantia de qualidade também são de baixíssimo custo, pois não dependem de constantes implementações.

Neste artigo irei explicar como escrever estes testes utilizando uma biblioteca chamada ArchUnit.

Apresentação do projeto

Para iniciarmos, vamos primeiro conhecer a estrutura base do nosso projeto.

Criou-se um simples programa usando Spring Boot contendo apenas uma entidade Pessoa e um controlador para comunicação externa. A estrutura se parece com isso:

Estrutura do projeto com Spring Boot mostrando pacotes controller e entity

Em nosso pom.xml, adicionamos a dependência do ArchUnit.


<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>0.18.0</version>
    <scope>test</scope>
</dependency>

Neste artigo iremos criar dois tipos de testes:

  • Um para validar se os controladores e entidades estão nos seus devidos pacotes.
  • Outro para verificar se os controladores tem o sufixo Controller e as entidades o sufixo Entity.

Sendo assim, iremos criar duas classes de testes, ambas ficarão dentro de um pacote chamado arch.

Estrutura do projeto mostrando o pacote arch com classes de teste

Criação dos testes

Vamos começar pelos testes de nomenclatura. Primeiramente em nossa classe NomenclaturaTest devemos dizer ao ArchUnit quais classes devem ser analisadas usando a anotação AnalyzeClasses e informando o pacote principal:


import com.tngtech.archunit.junit.AnalyzeClasses;

@AnalyzeClasses(packages="br.com.felixgilioli.testesarquiteturais")
public class NomenclaturaTest {

}

Também adicionaremos um import estático que ajudará a escrever os testes, retornando todas as classes que serão encontradas.


import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;

Diferente do JUnit onde criamos um método para cada teste, aqui iremos criar uma constante do tipo ArchRule que conterá a anotação ArchTest.

Agora devemos buscar todas as classes que contém a anotação RestController e verificarmos se ela tem o sufixo Controller, fazemos isso da seguinte forma.


@ArchTest
public static final ArchRule classesAnotadasComRestControllerDevemTerminarComOSufixoController = classes()
    .that().areAnnotatedWith(RestController.class)
    .should().haveSimpleNameEndingWith("Controller")
    .andShould().haveSimpleNameNotEndingWith("RestController")
    .as("Classes anotadas com @RestController devem ter o sufixo 'Controller'.");

O método classes() como foi explicado acima, retorna todas as classes que foram encontradas, já o that() nos permite filtrar apenas as classes que respeitam determinada condição, no nosso caso, todas que são anotadas com RestController. Por sua vez, o método should() faz a assertiva para validar se a classe termina com o sufixo Controller. Adicionamos outra assertiva através do andShould() para verificar se o nome não termina com RestController ao invés de apenas Controller. Caso o teste falhe, a mensagem descrita no método as() será exibida.

Vamos partir agora para o PackageTest. Adicionamos a mesma anotação AnalyzeClasses que foi apresentada acima.

Neste teste, iremos filtrar as mesmas classes que no teste anterior, porém o que mudará é a assertiva, desta vez queremos saber se essa classe se encontra no pacote controller, fazemos isso da seguinte forma.


@ArchTest
public static final ArchRule classesAnotadasComRestControllerDevemFicarNoPacoteController = classes()
    .that().areAnnotatedWith(RestController.class)
    .should().resideInAPackage("..controller..")
    .as("Classes anotadas com @RestController devem ficar no pacote 'controller'.");

Verificamos através do método resideInAPackage() se a classe esta naquele pacote.

O código final deve ficar desta forma.

NomenclaturaTest.java


package br.com.felixgilioli.testesarquiteturais.arch;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;

import javax.persistence.Entity;

import org.springframework.web.bind.annotation.RestController;

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

@AnalyzeClasses(packages = "br.com.felixgilioli.testesarquiteturais")
public class NomenclaturaTest {

    @ArchTest
    public static final ArchRule classesAnotadasComEntityDevemTerminarComOSufixoEntity = classes()
            .that().areAnnotatedWith(Entity.class)
            .should().haveSimpleNameEndingWith("Entity")
            .as("Classes anotadas com @Entity devem ter o sufixo 'Entity'.");

    @ArchTest
    public static final ArchRule classesAnotadasComRestControllerDevemTerminarComOSufixoController = classes()
            .that().areAnnotatedWith(RestController.class)
            .should().haveSimpleNameEndingWith("Controller")
            .andShould().haveSimpleNameNotEndingWith("RestController")
            .as("Classes anotadas com @RestController devem ter o sufixo 'Controller'.");
}

PackageTest.java


package br.com.felixgilioli.testesarquiteturais.arch;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;

import javax.persistence.Entity;

import org.springframework.web.bind.annotation.RestController;

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

@AnalyzeClasses(packages = "br.com.felixgilioli.testesarquiteturais")
public class PackageTest {

    @ArchTest
    public static final ArchRule classesAnotadasComEntityDevemFicarNoPacoteEntity = classes()
            .that().areAnnotatedWith(Entity.class)
            .should().resideInAPackage("..entity..")
            .as("Classes anotadas com @Entity devem ficar no pacote 'entity'.");

    @ArchTest
    public static final ArchRule classesAnotadasComRestControllerDevemFicarNoPacoteController = classes()
            .that().areAnnotatedWith(RestController.class)
            .should().resideInAPackage("..controller..")
            .as("Classes anotadas com @RestController devem ficar no pacote 'controller'.");

}

Caso alguém tenha interesse em ver o projeto inteiro, pode acessa-lo no meu GitHub. Para mais exemplos e casos de uso podem consultar o site oficial do ArchUnit.

Conclusão

Neste artigo fiz uma breve explicação sobre o que são testes arquiteturais e como implementa-los na prática. Espero ter te ajudado a entender um pouco sobre o que é e como funciona.

O que são testes arquiteturais?

Testes arquiteturais são verificações automatizadas que garantem que o código-fonte de um aplicativo segue as regras e padrões de arquitetura definidos pela equipe. Eles verificam aspectos como nomenclatura, organização de pacotes, dependências entre componentes e outras convenções estruturais.

O que é ArchUnit?

ArchUnit é uma biblioteca Java de código aberto que permite testar a arquitetura do seu código através de asserções simples e elegantes. Ela permite verificar se as classes seguem nomenclaturas específicas, residem em pacotes corretos, respeitam camadas arquiteturais e seguem padrões de dependência definidos.

Como implementar testes arquiteturais em um projeto Spring Boot?

Para implementar testes arquiteturais em um projeto Spring Boot, adicione a dependência do ArchUnit ao seu pom.xml, crie classes de teste anotadas com @AnalyzeClasses especificando o pacote principal do projeto, e defina regras usando constantes do tipo ArchRule anotadas com @ArchTest. As regras podem verificar aspectos como nomenclatura de classes, estrutura de pacotes e relações de dependência.