Testes de Arquitetura em Kotlin: Por Que o Konture Existe - Parte 2/3
A arquitetura em Kotlin tem duas visões da mesma arquitetura: o gráfico Gradle que decide o que pode ser vinculado, e o modelo de fonte Kotlin que decide o que o código realmente diz. O Konture existe para testar ambas.
Considere uma regra de um projeto Android:
O código de apresentação não deve expor DTOs de transporte no estado da tela ou em APIs voltadas para a UI.
Essa regra pode falhar em dois lugares diferentes.
Pode falhar no gráfico de build quando o módulo de apresentação é conectado diretamente a um módulo de transporte:
// feature/profile/presentation/build.gradle.kts
dependencies {
implementation(project(":core:network"))
}Também pode falhar na fonte:
package com.acme.profile.presentation
import com.acme.network.dto.UserDto
data class ProfileUiState(
val user: UserDto,
)Essas falhas estão relacionadas, mas não são exatamente as mesmas. A primeira é uma dependência de módulo Gradle física. A segunda é um import na fonte. Uma ferramenta séria para testes de arquitetura em Kotlin tem que entender ambas, porque sistemas multiplataforma são governados por ambos.
Essa é a razão pela qual o Konture existe.
As ferramentas existentes são úteis. O Konture não está tentando substituir o compilador, o linter, o test runner ou todas as bibliotecas de teste de arquitetura. O problema é que cada visão de um projeto Kotlin deixa algo fora.
A arquitetura em Kotlin se estende por essas visões.
Você pode ver isso nas demonstrações incorporadas. A demonstração Now in Android inclui 36 projetos Gradle em settings.gradle.kts, incluindo divisões de recurso API/implementation e um projeto dedicado :konture-test. A demonstração KotlinConf KMP inclui 9 projetos Gradle que abrangem núcleo compartilhado, backend, Android, desktop, web, admin e testes de arquitetura. Uma ferramenta com uma única visão ainda pode ser útil nesses projetos, mas não verá naturalmente todos os limites que o projeto usa.
Comparação de Ferramentas
A questão prática não é “Qual ferramenta é a melhor?” É “Qual ferramenta possui qual tipo de regra?”
Konture’s bet is narrow: Kotlin architecture needs a single test surface that can talk about Gradle modules and Kotlin source declarations together.
The Failure Modes That Motivated It
The most expensive architecture failures rarely start as “bad architecture.” They start as reasonable local fixes:
- A feature implementation imports another feature implementation because the API module does not expose one missing contract yet.
- A shared KMP module accepts one Android import during a deadline, then discovers later that desktop or iOS can no longer reuse the code cleanly.
- A public repository interface returns a database entity because the entity already has the needed fields.
- A backend route calls a repository directly because the service layer felt like ceremony for one endpoint.
Each failure creates a debt with interest. The next refactor has to pay down hidden coupling before it can make the intended change. The next platform target has to separate APIs that were supposed to be portable. The next reviewer has to reconstruct an architectural decision from scattered imports and build files.
Konture was designed for that class of failure: a rule that is not purely source-level and not purely build-level, but architectural because it sits between the two.
Bytecode Is Valuable, But It Is Not the Whole Kotlin Program
ArchUnit is mature and proven for JVM systems. If your architectural rule can be answered from compiled classes, bytecode analysis is often a strong fit.
Modern Kotlin projects often need more context than that.
Kotlin source constructs do not always map neatly to the source-level design a team wants to protect. Top-level functions and extension functions compile into generated holder classes. Inline functions move bodies into call sites. object, delegated properties, and compiler plugins can introduce generated structures that are important to the runtime but noisy for a source-level design rule.
That does not make bytecode analysis wrong. It means bytecode is the wrong primary lens for rules such as:
- O código
:feature:checkout:impldepende de um módulo de implementação irmão? - O código
commonMainimporta uma API do Android? - Uma assinatura de domínio público expõe um tipo de persistência?
- O pacote
implpermanece interno na fonte?
Essas são questões de arquitetura do Kotlin e Gradle, não apenas questões de classes JVM.
A varredura de fontes é útil, mas os diretórios não são o build
Os varredores de fontes voltados para Kotlin são bons em declarações, importações, nomes, anotações e visibilidade. Eles podem expressar muitas regras que ferramentas de bytecode tornam desconfortáveis.
Mas um diretório de fontes não é um projeto Gradle. Um nome de pacote não é uma dependência de módulo. Um diretório chamado api não é a mesma coisa que um módulo no qual outros projetos dependem através de api ou implementation.
Essa distinção é importante em projetos Android e Kotlin Multiplatform:
:app
:core:domain
:core:data
:feature:checkout:api
:feature:checkout:impl
:feature:profile:api
:feature:profile:impl
:shared
:androidApp
:iosAppO build já sabe quais módulos existem, quais conjuntos de fontes são conjuntos de produção e quais dependências de projeto foram declaradas. Testes de arquitetura devem usar essa informação em vez de reconstruí-la apenas a partir de convenções de nomenclatura.
Linter não são testadores de arquitetura
detekt e ktlint são excelentes em verificações locais. Eles não foram projetados para responder a perguntas de sistema inteiro:
- Este módulo depende de um módulo irmão proibido?
- Uma implementação de recurso tornou-se visível para outra implementação?
- O gráfico do projeto contém um ciclo?
- Uma API pública expõe um tipo de outra camada?
Esses não são problemas de formatação. São problemas de propriedade e dependência.
Modelo bidirecional do Konture
O Konture combina a visão de build e a visão de fontes.
A visualização de construção inclui:
- Módulos Gradle.
- Conjuntos de fontes.
- Diretórios de produção Kotlin.
- Dependências projetadas do projeto.
- Contexto de plugin aplicado.
A visualização de fontes inclui:
- Arquivos, pacotes e importações.
- Classes, interfaces, funções e propriedades.
- Visibilidade e anotações.
- Referências entre classes do projeto.
O Konture suporta afirmações isoladas e agrupadas. Use Konture.architecture { ... } quando regras de módulo e fonte relacionadas devem ser executadas como um contrato arquitetural:
Konture.architecture {
modules {
that().haveNamePath(":shared")
should().notDependOnModule(":androidApp")
}
classes {
that().resideInAPackage("..shared..")
should().onlyDependOnClassesInAnyPackage(
"..shared..",
"kotlin..",
"java..",
)
}
}A regra do módulo captura a dependência de construção física. A regra da fonte captura o padrão de referência no nível da fonte. Usadas juntas, elas cobrem uma fronteira que nem a inspeção de construção nem a inspeção de fontes podem possuir completamente.
O que é o Konture
Konture é uma biblioteca de teste de arquitetura Kotlin com duas partes coordenadas:
- Um plugin Gradle que captura a estrutura do projeto, conjuntos de fontes e dependências de módulo.
- Uma biblioteca de afirmações que permite às equipes escrever regras de arquitetura como testes Kotlin comuns.
Não requer um executador de teste personalizado. Os testes de arquitetura podem ser executados sob JUnit, Kotest, TestBalloon ou outro executador Kotlin/JVM que seu projeto já usa.
Konture também é arquitetura-agnosticista. Não prescreve Clean Architecture, MVVM, arquitetura hexagonal, fatiamento de recursos ou DDD. Essas são escolhas de design. O trabalho do Konture é tornar o design escolhido executável.
Essa distinção importa. Uma equipe Android pode proteger a API e os módulos de implementação dos recursos. Uma equipe backend pode proteger portas e adaptadores. Uma equipe KMP (Kotlin Multiplatform) pode manter código compartilhado livre de dependências de plataforma. Uma equipe de bibliotecas pode impedir que tipos de implementação sejam expostos em pacotes públicos.
O Konture deve codificar a política da equipe, não inventar uma.
Filosofia do Design de API
A DSL (Domain Specific Language) é intencionalmente próxima aos testes normais:
Konture.modules { ... }é para políticas de projeto Gradle e conjuntos de fontes.Konture.classes { ... },Konture.files { ... },Konture.functions { ... }eKonture.properties { ... }são para declarações de código-fonte.Konture.layered { ... }é para regras de pacotes direcionais legíveis.Konture.architecture { ... }agrupa regras relacionadas a módulos e fontes em um contrato.- Espaços funcionais como
scopeFromPackageescopeFromModulesão saídas de emergência para predicados personalizados.
Aquela forma é deliberada. As regras de arquitetura são revisadas por engenheiros que podem não trabalhar na ferramenta de arquitetura. Uma regra deve ler como um teste, falhar como um teste e viver ao lado do resto da suite de verificação.
O ponto de extensão é o predicado. Quando a DSL fluente for muito abstrata, uma equipe pode inspecionar importações, anotações, visibilidade, superclasses, caminhos de arquivo, conjuntos de fontes ou dependências de módulos diretamente e escrever uma afirmação focada. Isso mantém políticas incomuns no código do projeto em vez de forçar o Konture a crescer um termo para cada arquitetura da organização.
Os Três Trabalhos Estruturais
Os testes de arquitetura são mais valiosos quando protegem decisões que são caras para consertar posteriormente. Em sistemas Kotlin, essas decisões geralmente se agrupam em três trabalhos.
Consciência do Gradle é Engenharia de Plataforma
Equipes Kotlin frequentemente usam módulos para gerenciar a propriedade, o escopo de compilação e a independência das funcionalidades. Isso faz com que o gráfico do Gradle seja uma preocupação da plataforma, não apenas um detalhe do arquivo de construção.
Considere esta política comum:
Modularizações de implementação de funcionalidades não devem depender de outras modularizações de implementação de funcionalidades.
Se :feature:checkout:impl adicionar esta dependência:
implementation ( project ( ":feature:profile:impl" ))a construção pode ainda passar. A funcionalidade imediata pode até ser enviada mais rapidamente. Mas o gráfico de módulos agora diz que checkout está acoplado aos detalhes internos do perfil.
Isso tem consequências práticas:
- Uma mudança na implementação do perfil pode forçar mais trabalho downstream do que necessário.
- A reutilização de cache de construção se torna menos eficaz porque mudanças internas cruzam as fronteiras das funcionalidades.
- Refatorar detalhes internos do perfil se torna mais difícil porque outra funcionalidade agora pode depender deles.
- Os revisores precisam notar manualmente o deslocamento dos arquivos de construção.
Uma regra consciente do Gradle torna a fronteira executável:
Konture.modules {
that().haveNameMatching(":feature:**:impl")
should().onlyDependOnModules(
":feature:**:api",
":core:**",
":shared",
)
}Isso não é apenas "arquitetura limpa." É a saúde da construção e a propriedade codificadas como um teste.
Consciência de Origem Protege as Fronteiras Semânticas
Um gráfico Gradle limpo não prova semântica de origem limpa.
Por exemplo, :data pode depender corretamente de :domain para implementar interfaces do domínio. Mas um desenvolvedor ainda pode expor um modelo de persistência em uma API voltada ao domínio:
package com.acme.domain
import com.acme.data.UserEntity
interface UserRepository {
fun getUser(id: UserId): UserEntity
}O gráfico de construção sozinho pode não informar que isso está errado. O modelo de origem pode.
Testes de arquitetura podem inspecionar importações, pacotes, declarações, visibilidade e assinaturas:
Konture.classes {
that().resideInAPackage("..domain..")
should().onlyDependOnClassesInAnyPackage(
"..domain..",
"kotlin..",
"java..",
)
}Para frameworks externos, um predicado de importação personalizado pode tornar a política explícita:
Konture.scopeFromPackage("com.acme.domain")
.assertTrue("Domain must not import framework or persistence APIs") { cls ->
cls.imports.none { fqName ->
fqName.startsWith("android.") ||
fqName.startsWith("androidx.compose.") ||
fqName.startsWith("org.springframework.") ||
fqName.startsWith("jakarta.persistence.")
}
}scopeFromPackage("com.acme.domain") seleciona um prefixo de pacote concreto para afirmações personalizadas. Em contraste, resideInAPackage("..domain..") usa correspondência de pacotes curinga no repositório de classes fluente do Konture.
Isso é a metade do nível da fonte de governança de arquitetura: não apenas quais módulos podem se ligar, mas quais conceitos são permitidos em quais partes do código.
Tradeoffs e Modos de Falha
Testes de arquitetura merecem a mesma ceticismo que qualquer outro limite de produção. Regras ruins criam arrasto.
Modos comuns de falha:
- Regras excessivamente amplas: Proibir
kotlinx..de um domínio pode acidentalmente bloquear o uso legítimo de coroutines ou serialização. - Exceções ocultas: Excluir grandes pacotes legados pode fazer com que a regra pareça mais forte do que realmente é.
- Ruído de código gerado: Códigos-fonte gerados podem precisar de tratamento explícito para que a regra proteja o código escrito pelo autor.
- Complexidade KMP:
commonMain,androidMaineiosMaingeralmente precisam de políticas diferentes. - Projetos mistos Java/Kotlin: Uma regra de fonte Kotlin pode não cobrir código Java a menos que o projeto lidar com isso propositalmente.
- Manutenção de regras: A arquitetura evolui. Os testes devem evoluir com decisões de arquitetura deliberadas, não bloquear essas evoluções acidentalmente.
Esses trade-offs não enfraquecem o caso para testes de arquitetura. Eles definem a barreira para usar esses testes responsavelmente.
Por exemplo, uma regra KMP tentadora é:
Código compartilhado não deve importar kotlinx..Isso geralmente é muito amplo. kotlinx.coroutines pode ser uma dependência de código compartilhado legítima, enquanto uma importação do framework Android não é. A melhor regra é mais específica: proíba os pacotes de plataforma ou framework que realmente violam a portabilidade e permita as bibliotecas multiplataforma que a arquitetura usa intencionalmente.
Comece com regras estáveis e de alta sinalização. Faça exceções visíveis. Prove que cada regra pode falhar. Trate mudanças nas regras como alterações na arquitetura, não como uma maneira de deixar o CI verde.
Evidências das Suites Showcase
O repositório inclui suites showcase que exercitam o Konture em diferentes níveis de complexidade.
O menor showcase Gradle modela um projeto clássico :app, :domain e :data. Sua suite padrão de arquitetura contém 14 testes que cobrem o gráfico de módulos, as fronteiras dos pacotes-fonte, os contratos do repositório, a colocação das use cases, as regras de fuga de tipo e as regras de acesso. Também inclui um teste negativo que afirma que uma regra de módulo propositalmente errada lança uma AssertionError, o que é um padrão útil para provar que uma regra pode realmente falhar.
Os maiores showcases são mais representativos das preocupações da plataforma:
- Os arquivos de arquitetura do Android inspecionados contêm 13 testes verificando que os módulos de recursos não dependem de
:app, não contornam repositórios para alcançar módulos de banco de dados ou rede, mantêm módulos de API de recurso independentes dos módulos de implementação do recurso, prevenem a acoplamento entre implementações de recursos e mantém os ViewModels afastados das importações do framework Android. - Os arquivos de fronteira/back-end inspecionados do KotlinConf KMP contêm 8 testes verificando que o módulo compartilhado
:corepermanece uma dependência folha, os módulos de aplicativo cliente não dependem da implementação back-end, o código back-end não depende dos módulos do cliente front-end e as rotas back-end não importam diretamente repositórios ou esquemas de banco de dados.
Essas não são regras estilísticas de brinquedo. São versões executáveis das restrições de propriedade e plataforma: decoupling de recursos, pureza do modelo compartilhado, separação front-end/back-end, limites rota-serviço e controle da superfície API.
A demonstração leve do Gradle pode ser executada diretamente:
./gradlew -p showcases/sample-gradle :konture-test:testNeste repositório, esse comando é concluído com sucesso e executa o módulo de teste de arquitetura dedicado após gerar a estrutura do Konture e os metadados de dependência.
Desempenho e Escalabilidade
Os testes de arquitetura devem ser baratos o suficiente para que as equipes os mantenham no loop de feedback.
O plugin Gradle do Konture gera metadados de estrutura a partir da build e a biblioteca de asserções roda dentro da tarefa normal de teste. Isso tem algumas consequências práticas para projetos grandes:
- Mantenha os testes de arquitetura em um módulo dedicado para que os módulos de produção não herdem dependências apenas para teste.
- Escopo as regras aos módulos e pacotes que realmente governam em vez de procurar o projeto inteiro para cada afirmação.
- Prefira um número pequeno de contratos de alto sinal em vez de dezenas de regras de estilo sobrepostas.
- Deixe o Gradle lidar com as entradas da tarefa e a cacheabilidade dos metadados de layout gerado em vez de redescobrir o gráfico do projeto em cada teste.
- Monitore a duração da tarefa de teste de arquitetura no CI como qualquer outra verificação.
Para projetos com mais de 100 módulos, a pergunta importante não é apenas “A ferramenta pode escanear o repositório?” É “A equipe pode entender a falha e consertá-la rapidamente?”. Uma regra rápida com uma violação vaga ainda desperdiça tempo de revisão. Uma regra ligeiramente mais lenta que identifica o módulo proibido, importação ou assinatura pública geralmente é um investimento melhor na plataforma.
Feedback em Dois Vizualizações para Alterações Assistidas por IA
Assistentes de codificação baseados em IA tendem a otimizar pelo progresso local: importar a classe visível, adicionar a dependência ausente, satisfazer o teste imediato. O modelo de dois-vizualizações do Konture fornece-lhes sinais de reparo mais precisos. Uma falha no gráfico de compilação diz “você adicionou ou se baseou na borda errada do módulo”; uma falha no modelo-fonte diz “este arquivo importou ou expôs o conceito errado”. Essas são correções diferentes, e a saída dos testes deve tornar essa distinção visível.
Quando o Konture é uma Boa Opção
O Konture é uma boa opção quando as regras que você se importa estão espalhadas entre o código-fonte Kotlin e a estrutura Gradle:
- Limites de módulos do Gradle e gráficos de projetos acíclicos.
- Separação da funcionalidade
:apie:impl. - Superfícies de apresentação, domínio e API pública permanecendo independentes das tipagens de transporte, persistência e quadros.
- Assinaturas da API pública evitando tipos de persistência, transporte ou UI.
- Convenções de visibilidade do Kotlin como manter pacotes de implementação
internal. - Convenções de arquivos e pacotes que exigem contexto do projeto.
É menos útil para formatação, estilo comum e verificações que um linter padrão já trata bem. Use a ferramenta mais barata que possa impor a regra de forma precisa.
Limites Atuais e Pressão da Estratégia de Desenvolvimento
O Konture deve ser explícito sobre o que não é.
Não é uma substituição para o compilador Kotlin, análise de bytecode, testes de integração em tempo de execução ou varredura de vulnerabilidades de dependência. Não é o lugar certo para verificar comportamentos ocultos por reflexão ou contêineres de DI em tempo de execução. Projetos mistos Java/Kotlin precisam de cobertura deliberada porque a análise do código-fonte Kotlin não faz automaticamente a arquitetura Java visível. Fontes geradas e saídas de plugins do compilador podem precisar de exclusões ou regras separadas para que o conjunto foque na arquitetura escrita.
A evolução da ferramenta de compilação também importa. O Android Gradle Plugin, modelagem de source-set do Kotlin Multiplatform, mudanças no compilador Kotlin e novos padrões de código gerado podem todos alterar o que “a estrutura do projeto” significa. O valor a longo prazo do Konture depende de permanecer honesto sobre essas entradas: metadados do Gradle, análise de fontes Kotlin, consciência source-set/plataforma e mensagens de falha que ajudem as equipes a corrigir o design em vez de lutar contra a ferramenta.
A pressão da estratégia de desenvolvimento é, portanto, prática:
- Fonte de conjunto KMP mais profunda e exemplos conscientes da plataforma,
- Tratamento mais claro do código gerado,
- Cheques públicos de fuga de API mais fortes,
- Diagnósticos melhores para conjuntos grandes de regras,
- Integração suave com cache de build e relatórios CI.
A Ideia Central
A arquitetura não deve se basear na memória.
Se uma fronteira é importante o suficiente para ser protegida em cada revisão, ela pode ser um candidato a um teste executável. Se quebrá-la lentifica as builds, expõe APIs, acorrenta equipes ou torna mudanças assistidas por IA mais arriscadas, o repositório deve poder dizer isso.
O Konture existe porque a arquitetura Kotlin não está apenas em bytecode, nem apenas em arquivos de origem e nem apenas em arquivos de build Gradle. Ela vive na relação entre eles.

