Testes de Arquitetura em Kotlin: O Que São e Por Que Importam - Parte 1/3
A um projeto Kotlin pode compilar, passar seus testes unitários, satisfazer seu linter e ainda se tornar estruturalmente mais difícil de mudar. Testes de arquitetura existem para preencher essa lacuna.
A maioria das ferramentas de verificação responde a perguntas locais.
O compilador pergunta se o código é válido Kotlin. Um linter pergunta se um arquivo segue regras locais de estilo e qualidade. Testes unitários perguntam se uma função ou componente comporta-se conforme esperado.
Essas verificações são necessárias. Elas não são as mesmas perguntas que se fazem sobre a forma do sistema ainda depender da equipe.
Considere um caso de uso no domínio que começa a depender de uma implementação da camada de dados:
pacote com.acme.domain
import com.acme.data.SqlUserRepository
class GetUserUseCase(
private val repository: SqlUserRepository,
)Este código pode compilar. Os testes unitários podem passar. ktlint pode não ter nada útil a dizer.
O problema é estrutural: a camada de domínio agora conhece um detalhe de persistência. Uma fronteira que deveria preservar a mudabilidade se tornou uma convenção que as pessoas precisam lembrar.
Testes de arquitetura transformam essa convenção em uma regra executável:
Konture.classes {
que().resideInAPackage("..domain..")
deve().onlyDependOnClassesInAnyPackage(
"..domain..",
"kotlin..",
"java..",
)
}Essa é a ideia principal. Testes de arquitetura não provam que o software está correto. Eles provam que decisões estruturais específicas ainda são verdadeiras.
A Ilusão da Construção Verde
Uma construção verde diz que o repositório satisfez as verificações que você pediu para executar.
Não diz que a arquitetura pretendida sobreviveu à mudança.
O compilador aceitará uma dependência proibida se o símbolo estiver no classpath. O Gradle construirá um gráfico de módulos que viola seu design se alguém declarar a dependência. Um teste unitário não falhará porque um módulo de recurso importou um detalhe da implementação do outro módulo de recurso, a menos que o comportamento testado mudasse.
Muitas vezes, as violações de arquitetura parecem normais em revisões:
- Um controlador chama diretamente um repositório porque foi mais rápido do que adicionar uma aplicação de serviço.
- Um modelo de domínio aceita um DTO da rede porque o DTO já tem os campos corretos.
- Um módulo de implementação de recurso importa outro módulo de implementação de recurso porque o módulo API ainda não expõe o contrato necessário.
- Uma classe Kotlin em um pacote
implpermanece pública por padrão e se torna conveniente para outros módulos reutilizarem. - Um assistente de codificação AI adiciona uma dependência Gradle porque isso faz com que o arquivo atual compile.
Nenhuma dessas mudanças precisa ser maliciosa ou negligente. A maioria das alterações estruturais vem de pequenas otimizações locais que são racionais no momento e caras em agregado.
Testes de arquitetura são uma maneira de tornar o custo agregado visível cedo.
A Difta Estrutural Tem Forma
A maioria da deriva de arquitetura não é dramática. Geralmente começa como um detalhe que parece razoável em uma solicitação de pull.
O segundo gráfico ainda pode compilar. O comportamento do produto pode ainda ser correto. O dano aparece mais tarde:
- Uma refatoração de perfil agora precisa de contexto de checkout,
- Mudanças em recursos não relacionados invalem mais trabalho de construção,
- Os revisores têm que raciocinar sobre um raio de explosão maior,
- A próxima atalho parece menos incomum porque o primeiro já existe.
As métricas certas são específicas do projeto, mas as úteis são concretas: número de bordas de módulo proibidas, número de violações de regras por mês, fã-in e fã-out do módulo, escopo de rebuild após uma mudança em recurso e comentários de revisão repetidos sobre o mesmo limite. Os testes de arquitetura se tornam persuasivos quando transformam essas observações em um exemplo falho em vez de um argumento de estilo.
O Que os Testes de Arquitetura Verificam
Bons testes de arquitetura protegem decisões que afetam a velocidade de mudança, independência do módulo, forma da API pública e carga de revisão. Eles geralmente se enquadram em algumas categorias.
1. Direção de Dependência e Isolamento de Camadas
Sistemas em camadas dependem da direção. Na Clean Architecture, ports e adapters, e muitos designs centrados no domínio, as camadas externas podem depender para dentro, mas o núcleo não deve depender de UI, bancos de dados, frameworks de transporte ou APIs de plataforma.
Regras típicas:
- Pacotes de domínio não devem importar persistência, transporte, Android, Compose, Spring ou APIs do servidor Ktor.
- Serviços de aplicação podem depender de contratos de domínio, não diretamente em controladores web.
- Módulos UI devem consumir estado de apresentação, não entidades de banco de dados ou rede.
O compilador vê tipos válidos. Testes de arquitetura codificam quais tipos válidos são inaceitáveis em uma camada dada.
2. Limites de Módulos Gradle
Em um projeto Kotlin modular, dependências do projeto Gradle fazem parte da arquitetura.
Regras típicas:
:core:domainnão deve depender de:core:dataou:app.- Módulos de implementação de recursos não devem depender de módulos irmãos de implementação de recursos.
- Módulos API podem ser dependidos amplamente; módulos de implementação devem permanecer atrás de sua API.
- O gráfico do projeto Gradle não deve conter ciclos.
Esta categoria importa para o design e para a performance da build. Módulos de camada desnecessários expandem o escopo de recompilação, reduzem a utilidade do cache e fazem com que alterações locais afetem recursos não relacionados.
3. API Pública e Fuga de Tipos
Alguns erros de arquitetura não são sobre imports em uma implementação privada. Eles estão relacionados a o que um módulo expõe.
Regras típicas:
- APIs de domínio público não devem expor entidades de banco de dados ou DTOs da rede.
- Pacotes de API de recurso público devem expor contratos e modelos estáveis, não classes de implementação.
- Anotações de persistência ou quadros de trabalho não devem se infiltrar em interfaces limpas de negócios.
- Módulos de biblioteca devem manter pacotes de implementação
internala menos que sejam intencionalmente públicos.
A visibilidade padrão public do Kotlin torna isso fácil de fazer errado. Uma vez que outro módulo começa a depender de um tipo público acidental, removê-lo se torna uma mudança quebradora.
4. Chamadas entre Camadas
Alguns sistemas dependem de uma camada intermediária para validação, autorização, transações, log, ou orquestração. Uma chamada direta pode contornar o lugar onde essas políticas residem.
Regras típicas:
- Controladores chamam serviços de aplicação, não repositórios diretamente.
- Composables chamam ViewModels ou presenters, não serviços Retrofit.
- Manipuladores de roteiros chamam casos de uso, não adaptadores SQL.
- Módulos UI não chamam módulos de infraestrutura.
Essas regras devem ser usadas com cuidado. Elas são valiosas quando a camada intermediária tem uma responsabilidade real. São burocracia quando a camada existe apenas porque um diagrama diz que sim.
5. Convenções de Injeção de Dependência e Encaminhamento
A configuração DI é arquitetura em forma executável. Decide qual implementação sustenta qual contrato.
Algumas políticas de encaminhamento pertencem a testes de integração. Outras podem ser verificadas estruturalmente:
- Módulos DI residem em pacotes aprovados.
- Módulos de recurso não sobrescrevem vinculações do núcleo.
- Implementações de adaptador são vinculadas a interfaces de domínio em vez de serem consumidas diretamente.
- Vinculações apenas para testes não se infiltram nos conjuntos de fonte de produção.
A regra útil é a que captura uma classe real de falhas em produção ou manutenção, não aquela que apenas reflete uma preferência.
6. Higiene de Arquivos e Fontes
Não precisa ser uma análise profunda para cada teste estrutural. Algumas regras mantêm a navegação previsível e reduzem o ruído na revisão:
- Uma classe primária por arquivo.
- Os nomes dos arquivos devem corresponder aos nomes das classes principais.
- Nenhuma importação com wildcard.
- Pacotes gerados ou de migração são explicitamente excluídos.
Essas regras não devem duplicar o que um formataador ou linter já faz bem. Os testes de arquitetura são mais valiosos quando precisam do contexto do projeto inteiro.
O que os Testes de Arquitetura Não Deveriam Verificar
Os testes de arquitetura se tornam britânicos quando tentam governar tudo.
Eles não devem substituir:
- O compilador para segurança de tipos.
- Um formataador para espaços em branco e estilo.
- Um linter para cheiros comuns de um único arquivo.
- Testes unitários para comportamento.
- Testes de integração para conexões reais e comportamentos em tempo de execução.
- Revisão de código para julgamento, nomes e intenção de design que não são estáveis o suficiente para codificar.
Um mau teste de arquitetura congela um detalhe da implementação e o chama de design. Um bom teste protege uma fronteira que vários engenheiros já dependem.
Como os Testes de Arquitetura Tornam-se Destructivos
Os testes de arquitetura são governança. Uma má governança é pior do que nenhuma governança porque ensina as pessoas a contornar o sistema.
Modos comuns de falha:
- Regras Frágeis: um teste codifica a estrutura atual dos diretórios em vez de uma fronteira durável.
- Segurança Falsa: um conjunto de testes passando é tratado como prova de que a arquitetura é boa.
- Friction entre Equipes: uma regra bloqueia o trabalho legítimo de recursos, mas ninguém sabe quem é responsável pelo processo de exceção.
- Testes Exagerados para Código Gerado: Room, KSP, Compose, serialização ou fontes geradas por DI criam ruído que as regras de código autorizado nunca foram destinadas a julgar.
- Proibição Excessiva de Pacotes: uma regra bloqueia muito, então engenheiros adicionam exclusões até que a regra não signifique mais nada.
- Sem Prova Negativa: o conjunto de testes nunca falhou contra uma violação intencional.
O antídoto não é um conjunto maior de testes. É um conjunto menor e mais explícito. Cada regra deve nomear a decisão que protege, o custo de violá-la e o caminho intencionado para reparação.
Quando Não Usar
Os testes de arquitetura não são automaticamente valiosos.
Eles geralmente são prematuros para um códigobase pequeno onde todos ainda podem manter a estrutura em mente. Podem ser barulhentos em produtos na fase inicial, onde as fronteiras de módulos e pacotes estão sendo descobertas semanalmente. Também podem não se encaixar bem em códigos altamente dinâmicos ou reflexivos, onde o dependência significativa é a configuração do tempo de execução em vez da estrutura de fontes visíveis. Eles também podem retardar um monolito com muita rotação se o primeiro conjunto tentar impor a arquitetura alvo em vez da que a equipe está realmente migrando.
Nesses casos, ferramentas mais leves podem ser suficientes:
- um registro curto de decisões de arquitetura,
- uma nota sobre a propriedade do módulo,
- um checklist de revisão para as próximas mudanças,
- um relatório não bloqueante que conta violações antes de impô-las.
Use testes de arquitetura quando a fronteira é estável o suficiente para ser imposta e caro o suficiente para ser violada.
Documentação Viva que Falha
Diagramas de arquitetura, READMEs, documentos de integração e checklists de revisão ajudam. Nenhum deles falha no CI.
Um teste de arquitetura dá uma forma durável a uma regra:
@Test
fun `domínio não deve depender dos módulos de dados ou aplicativo`() {
Konture.modules {
que().tenhaCaminhoNome("":domain")
deve().nãoDependerDoMódulo("":data")
deve().nãoDependerDoMódulo("":app")
}
}O nome do teste documenta a regra. A afirmação define a regra. A saída de falha informa ao desenvolvedor onde a regra foi violada.
Violação de arquitetura(s) detectada:
O módulo :domain não deve depender do :data, mas uma dependência foi encontrada.A equipe ainda decide se a regra está correta. O teste remove a necessidade de redescobrir a mesma violação manualmente.
Impacto Organizacional
O efeito técnico é a imposição de limites. O efeito organizacional é a memória compartilhada.
Para novos engenheiros, testes de arquitetura comprimem a integração. Eles mostram quais dependências são permitidas, quais APIs são intencionalmente públicas e onde as exceções vivem. Para engenheiros experientes, eles reduzem comentários repetitivos em revisões de código para que a revisão possa se concentrar no julgamento do design ao invés de policiar os mesmos imports.
Para equipes, a escolha é entre autonomia e consistência. Um grupo de plataforma ou arquitetura não deve usar testes para centralizar cada decisão local. O padrão melhor é codificar um pequeno conjunto de contratos transversais às equipes e deixar as equipes de recursos assumir o resto. Quando uma regra muda, trate essa mudança como uma decisão de arquitetura: atualize o teste, atualize a ADR ou os documentos se existirem, e faça o caminho da migração explícito.
Em escala, testes de arquitetura funcionam melhor como parte da governança, não como uma substituição para ela:
- ADRs explicam por que um limite existe.
- Testes de arquitetura verificam se o limite ainda é mantido.
- Relatórios CI indicam onde o limite foi cruzado.
- Revisores decidem se o código ou o limite deve ser alterado.
Um Exemplo Concreto das Apresentações
O repositório Konture inclui uma pequena demonstração Gradle que usa a mesma estrutura dos exemplos acima: :app, :domain, :data e um módulo dedicado :konture-test.
Sua suite de arquitetura não verifica apenas uma regra feliz. Ela combina vários controles estruturais:
- O gráfico de módulos não possui ciclos.
:domainnão depende de:dataou:app.:datadepende apenas de:domain.- Classes em
..domain..dependem apenas de pacotes domain, Kotlin ou Java. - Declarações de repositório no domínio são interfaces.
- Assinaturas de caso de uso não expõem tipos
.data.ou.app..
Um teste deliberadamente prova que uma regra de módulo ruim lança um AssertionError. A afirmação intencionalmente errada diz que :data deve depender apenas de :app; o projeto de amostra tem :data dependendo de :domain, então a regra falha com a mesma forma que uma regressão arquitetural real:
Detecção de violação(s) da arquitetura:
Módulo :data depende de :domain, o que não é permitido pelo padrão(s): :appIsso importa. Uma regra estrutural que nunca falhou pode não estar verificando o que a equipe pensa que está verificando.
O conjunto de amostras é executável:
./gradlew -p showcases/sample-gradle :konture-test:testNeste repositório, esse comando executa com sucesso o módulo de teste arquitetônico e gera a disposição e os metadados de dependência que o Konture usa para avaliar as regras conscientes de módulos.
Por Que Isso Importa com o Desenvolvimento Assistido por IA
Os assistentes de codificação baseados em IA são bons em conclusão local. Eles podem importar uma classe visível, adicionar uma dependência ausente e fazer um teste estreito passar.
A arquitetura geralmente é contexto global.
Instruções como essa ajudam:
Manter o domínio independente de dados.
Não adicionar dependências de recursos laterais.
Mapear DTOs da rede antes que eles alcancem o estado UI.Mas as instruções de prompt não são uma forma de imposição. Elas são orientação.
Os testes de arquitetura dão ao mesmo tempo um loop de feedback para humanos e agentes:
- Uma mudança atravessa uma fronteira.
- O teste falha com um módulo concreto, arquivo, importação ou tipo.
- O desenvolvedor ou agente conserta o design usando a abstração pretendida.
Isso não é mágica e não é uma substituição para revisão. É uma maneira de tornar regras estruturais visíveis às ferramentas que já estão alterando o código.
Pressão Futura na Arquitetura Kotlin
A necessidade de feedback estrutural provavelmente aumentará, não diminuirá.
O Compose Multiplatform torna o código de IU portável, mas também cria novas perguntas sobre quais abstrações de IU pertencem ao código compartilhado e quais permanecem específicas da plataforma. O Kotlin 2.x e pilhas baseadas em compilador continuam a desfocar a linha entre o código-fonte escrito e o gerado ou transformado. Agentes IA podem produzir grandes patches localmente plausíveis mais rápido do que um revisor pode inspecionar cada borda de módulo à mão.
Isso não significa que toda equipe precisa de mais regras. Significa que as importantes regras precisam ser executáveis, estreitas e fáceis de consertar. O conjunto de arquitetura amigável ao futuro não é o maior. É aquele que pega as fronteiras humanas e agentes são mais propensos a cruzar acidentalmente.
Quando uma Regra Merece Enforcamento do CI
Não toda boa ideia deve bloquear uma construção. Uma regra é um bom candidato para CI quando a maioria desses são verdadeiros:
- A equipe pode explicar o custo de quebrá-la.
- A regra é estável durante trabalho normal de recursos.
- Violações geralmente são erros, não escolhas legítimas de design.
- A mensagem de falha aponta para uma correção açãoável.
- Exceções são raras e podem ser nomeadas explicitamente.
- A regra pega algo que o compilador, linter ou testes unitários não pegam.
Se uma regra falhar constantemente durante o desenvolvimento normal, pode ser muito ampla. Se uma regra precisar de muitas exclusões silenciosas, pode estar fingindo que a arquitetura é mais limpa do que realmente é. Se ninguém pode explicar por que ela existe, não deve bloquear a entrega.
Comece com regras que protejam dores conhecidas: ciclos de módulos, dependências de domínio para dados, acoplamento de implementação de recursos, vazamento de API pública e APIs da plataforma vazando em código compartilhado KMP.
O Retorno Prático
Testes de arquitetura ajudam equipes a preservar a estrutura que torna as mudanças futuras mais baratas:
- Eles mantêm a lógica do domínio independente de frameworks e persistência.
- Eles evitam bordas acidentais do Gradle que ampliam a recompilação.
- Eles protegem módulos de API contra vazamento de detalhes de implementação.
- Eles tornam a visibilidade pública intencional.
- Eles transformam comentários de revisão repetidos em verificações que falham com módulos, arquivos, importações ou tipos específicos.
O objetivo não é uma arquitetura rígida. O objetivo é uma arquitetura explícita.
Uma vez que a equipe tenha escolhido uma estrutura, o build deve ajudar a protegê-la.
O próximo artigo foca na parte que este guia apenas sugeriu: por que uma regra de fonte somente falha em capturar o derivação de dependência do Gradle, por que uma regra do Gradle somente falha em capturar vazamento de tipo no nível da fonte e por que os testes de arquitetura Kotlin precisam de ambas as perspectivas ao mesmo tempo.

