DevOps

Blindando a arquitetura de software com ArchUnit

Blindando a arquitetura de software com ArchUnit

O que é? 

Blindando a arquitetura de software com ArchUnit -Testes de arquitetura de software são testes que rodam de maneira automática, como testes de unidade ou integração, através de análise estática do código-fonte.

Ou seja, a aplicação não tem que estar executando para executar os testes.

Por que criar testes automatizados para a arquitetura de software?

arquitetura
Os testes automatizados dão muito mais seguranças para seus projetos

Imagine que você trabalha na empresa ACME como desenvolvedor de software, até que um belo dia é apresentado um novo projeto para você.

Nesse projeto fictício, você será o único desenvolvedor, o famoso “exército de um homem só”.

Então começa a jornada, levanta requisitos funcionais e não funcionais, define M.V.P.​, arquitetura e tecnologias desse pontapé inicial. 

O projeto segue evoluindo e você consegue manter a estrutura nos trilhos e tem orgulho do código-fonte que está ali.

Então a empresa decide investir mais no projeto e entra um novo desenvolvedor de equipe. 

Precisa gastar tempo com treinamento e os primeiros code reviews viram ping-pong (vão e voltam).

Você sai do projeto.

O projeto segue assim com a vida sem todo o seu conhecimento, o outro desenvolvedor não tinha o total domínio e mudanças sutis começam ocorrer.

Teoria das janelas quebradas

Entra mais pessoas na equipe e a teoria das janelas quebradas é evidenciado aqui.

Você também pode ler o livro O Ponto da Virada por Malcolm Gladwell, que procura entender alguns fenômenos sociais como esse que vou descrever a seguir.

A teoria das janelas quebradas basicamente é:

Se uma janela de um prédio for quebrada e não consertarem logo, a tendência é que comecem a jogar pedras nas outras janelas.

E dessa forma, passem a ocupar o edifício e destruir o restante.

Essa teoria está relacionada com a arquitetura inicial.

E é seguida em partes, o restante é descartado por falta de conhecimento e aparece diversos padrões para resolver uma mesma necessidade.

O software que antes seguiam a arquitetura bem definida, agora existem micro arquiteturas por todo o código fonte.

A casa bonita anteriormente, agora parece um monte de puxadinho.

Que técnica vai ser usada? Que tipo de teste você poderia ter utilizado lá no início?

Testes de arquitetura ao resgate!

OK, deixo claro que, apesar de ser um ótimo tipo de teste, não é bala de prata.

Vem para agregar qualidade porque código mal escrito pode levar o software para o buraco mesmo com sua estrutura bem definida e com testes sólidos.

O teste de arquitetura de software 

O conceito é relativamente simples para alguém que já esteja familiarizado com testes automáticos.

Pode tranquilamente executar junto com o passo de teste da sua pipeline de build deploy.

Mas como esses testes ajudariam a manter a sanidade da arquitetura?

Arquitetura nos apresenta um conceito chamado de regra.

Essas regras estáticas funcionam como uma espécie de contrato. 

Se nenhuma regra se violar, o teste passa com sucesso.

Mas caso algum código que teve alteração passar por violação dessas regras, o teste quebra e informa a não conformidade criada.

Podemos definir diversos tipos de regras

Como os componentes se constroem e como eles se relacionam entre si.

Falando de orientação a objetos, podemos dizer que classes que são Controller precisam ter uma constante NAME por definição, extender a classe BaseController e estarem dentro do diretório controller

Ainda por cima, podemos dizer que nenhuma classe do sistema deve depender de controller.

Ou que classes controller só podem acessar classes do diretório services por exemplo.

O que é ArchUnit?

ArchUnit é a melhor opção para você?

ArchUnit é uma biblioteca para código Java.

Feita para facilitar a criação de testes de arquitetura de software usando o suporte do JUnit para executar seus testes, provendo diversas funcionalidades extremamente úteis

Antes de mostrar alguns exemplos, vamos ver como é sintaxe de uma definição estrutural de uma regra com ArchUnit:

(structure) that (predicate) should (condition).

Structure: Qual o componente será feito o teste: Classe, método, campo, etc…
Predicate: Qual o filtro quer aplicar sobre a estrutura, qual característica de interesse para a regra.
Condition: Qual a asserção/checagem que precisa ser verdade.

@Test
public void controllersDevemPertencerAoPacoteEntryPoint() {
// Structure: Quero testar classes.
ArchRule archRule = ArchRuleDefinition.classes()
// Predicate: Quero apenas classes que o simple name finalize com Controller. Exemplo: BillingController.java.
.that().haveSimpleNameEndingWith("Controller")
// Condition: Quero checar que as classes filtradas pertencem a esse pacote ou pacotes filhas. (os dois pontos antes e depois significa qualquer coisa, ArchUnit utiliza definição AspectJ Pointcuts)
.should().resideInAnyPackage("..entrypoint..");

Essa é a definição de quais pacotes eu quero aplicar a regra.

No caso, estou colocando aqui no br.com.foo.bar que consideramos como o pacote raíz do projeto.

Ou seja, importe todas as classes desse pacote e adjacentes para testes, a opção DO_NOT_INCLUDE_TESTS informa que não é para incluir classes do ambiente de teste.


JavaClasses javaClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("br.com.foo.bar");
// Aplica a regra nas classes importadas.
archRule.check(javaClasses);
}

Traduzindo para o bom português

Classes que tem nome simples finalizando com Controller devem residir em qualquer pacote ..entrypoint.., fluido, não? 🙂

Boa parte da API utiliza essa estrutura, exceto alguns tipos de testes de isolamento de camada que mostrarei ainda nesse artigo.

ArchUnit também permite o uso de lógica booleana aumentando a flexibilidade em definir regras mais complexas.

ArchRule archRule = ArchRuleDefinition.classes()
.that().haveSimpleNameEndingWith("Controler")
.or().areAnnotatedWith(Controller.class)
.should().resideInAnyPackage("..entrypoint..")
.orShould().resideInAnyPackage("..controller.");

Vamos ver alguns tipos de testes que podemos criar com ele:

  • Herança

classes().that().implement(Connection.class)
.should().haveSimpleNameEndingWith("Connection")

  • Dependência entre pacotes

classes().that().resideInAPackage("..foo..")
.should().onlyHaveDependentClassesThat().resideInAnyPackage("..source.one..", "..foo..")

  • Anotações

classes().that().areAssignableTo(EntityManager.class)
.should().onlyBeAccessed().byClassesThat().areAnnotatedWith(Transactional.class)

  • Isolamento de camadas

layeredArchitecture()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")

.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")

Com as regras podemos amarrar a arquitetura para que o sistema continue evoluindo e a base mantenha padronizada e sólida.

Apenas tomar cuidado ao definir regras que são fora do escopo que irá acabar engessando e tornar a arquitetura pouca dinâmica.

Existe ponto negativo?

Sempre tem os dois lados da moeda!

Pois bem, um ponto negativo é caso uma evolução na arquitetura que será feita de maneira gradual.

O teste terá que dar suporte a versão antiga e a versão que está sendo implantada, podendo aumentar a complexidade do teste.

Mas nada que não seja impossível de desenvolver!

Ainda assim os pontos positivos se sobressaem como mais uma carta na manga em busca do software sólido.

Leia também “Teste de Integração