Sua test suite verifica que calculateTotal retorna 42 quando recebe a entrada correta. Ela não verifica se src/domain/Invoice.ts tem permissão para importar src/infrastructure/Database.ts. O compiler aceita ambos. Seus testes unitários aceitam ambos. Mas um deles é uma violação arquitetural que vai custar uma semana de refatoração daqui a seis meses.
Esse é o ponto cego. Escrevemos testes para lógica e assumimos que a estrutura se cuida sozinha. Não se cuida.
O que é, na prática, um teste de arquitetura
Um teste de arquitetura é uma asserção sobre a estrutura do seu código, não sobre seu comportamento. Ele verifica se o grafo de dependências corresponde ao design que vocês combinaram. Ele quebra o build quando um desenvolvedor importa a camada errada, cria uma dependência circular ou nomeia uma classe de repository sem o sufixo que vocês padronizaram.
Essas não são regras de lint. Linting pega violações de estilo. Testes de arquitetura pegam violações estruturais. A diferença importa porque estrutura tem semântica. Um module que importa seu pai não é um problema de estilo. É um problema de design.
A maioria das equipes documenta essas regras em uma wiki, um README ou uma mensagem fixada no Slack pelo tech lead. Documentação é útil para onboarding. É inútil para garantir que sejam seguidas. Um teste de arquitetura move a regra da memória humana para o pipeline de build, onde esquecê-la custa um build vermelho em vez de um incident em produção.
O que você pode impor com um teste
O caso de uso óbvio é a direção de dependências. Domain não deveria importar infrastructure. UI não deveria importar data access diretamente. Essas regras se mapeiam claramente para limites de packages ou diretórios.
Mas testes de arquitetura podem verificar mais do que imports. Aqui estão padrões que realmente importam em codebases de produção.
Dependências cíclicas. Um package que se importa através de uma cadeia de três outros packages ainda é cíclico. Seus olhos não vão pegar isso em um code review. Um teste que percorre o grafo de imports vai.
Convenções de nomenclatura. Se sua equipe decidiu que toda implementação de repository deve terminar em Repository, um teste pode impor isso. Parece pedante até alguém criar UserDao e UserRepo na mesma codebase e novos engenheiros não saberem qual usar.
Dependências proibidas em bibliotecas específicas. Talvez sua camada de domínio não tenha permissão para depender de axios, pg ou fs. Talvez seu frontend não tenha permissão para importar lodash porque você está padronizando em métodos nativos. Um teste de arquitetura pode assegurar que um package específico nunca apareça na árvore de dependências de um module específico.
Regras de anotação e herança. Em Java, você pode testar que nenhuma classe em ..domain.. usa @Autowired. Em C#, você pode testar que nenhuma classe em Infrastructure implementa uma interface de Application. Essas são restrições estruturais que análise estática sozinha não consegue expressar sem conhecimento de domínio.
Como escrever um
Os melhores testes de arquitetura parecem testes unitários comuns. Eles rodam no seu test runner existente. Eles aparecem no mesmo job de CI que seus outros testes. A única diferença é sobre o que eles asseguram.
Java com ArchUnit:
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
@AnalyzeClasses(packages = "com.mycompany")
class ArchitectureTest {
@ArchTest
static final ArchRule no_cycles =
slices()
.matching("com.mycompany.(*)..")
.should().beFreeOfCycles();
@ArchTest
static final ArchRule domain_is_independent =
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAPackage("..infrastructure..");
}
Esse é um teste JUnit. Ele roda com mvn test. Ele quebra o build. Não é necessário um job de CI especial.
C# com NetArchTest:
using NetArchTest.Rules;
using Xunit;
public class ArchitectureTests
{
[Fact]
public void Domain_Should_Not_Depend_On_Infrastructure()
{
var result = Types.InCurrentDomain()
.That()
.ResideInNamespace("MyApp.Domain")
.ShouldNot()
.DependOnAny(Types.InNamespace("MyApp.Infrastructure"))
.GetResult();
Assert.True(result.IsSuccessful);
}
}
Python com import-linter:
Python não tem um equivalente maduro do ArchUnit, mas import-linter te dá uma config declarativa que funciona como um teste:
# .importlinter
[importlinter:contract:domain-independent]
name = Domain does not import infrastructure
type = forbidden
source_modules =
myapp.domain
forbidden_modules =
myapp.infrastructure
Rode com lint-imports no CI. Ele sai com código diferente de zero em caso de violação.
Go: escreva o seu próprio
Go não tem uma biblioteca mainstream de testes de arquitetura. A maioria das equipes escreve um teste pequeno que percorre a AST:
import (
"strings"
"testing"
"golang.org/x/tools/go/packages"
)
func TestDomainDoesNotImportInfrastructure(t *testing.T) {
cfg := &packages.Config{
Mode: packages.NeedImports | packages.NeedFiles,
}
pkgs, err := packages.Load(cfg, "./domain/...")
if err != nil {
t.Fatal(err)
}
for _, pkg := range pkgs {
for path := range pkg.Imports {
if strings.Contains(path, "/infrastructure/") {
t.Errorf("domain package %s imports infrastructure: %s", pkg.PkgPath, path)
}
}
}
}
São vinte linhas. Ele vive na sua test suite. Ele roda em todo PR. Esse é o ponto. Você não precisa de um framework. Você precisa de uma asserção.
Os trade-offs que ninguém comenta
Testes de arquitetura não são de graça. Eles introduzem uma nova categoria de falha de build, e novas categorias de falha de build sempre criam fricção.
Eles são mais lentos que testes unitários. Um teste de arquitetura que escaneia todo import em uma codebase de 500 mil linhas leva tempo. Não horas. Segundos, às vezes dezenas de segundos. Mas isso é uma ordem de magnitude mais lento que um teste unitário típico. Se você rodá-los no mesmo job que seus testes unitários rápidos, você perde o feedback loop que torna os testes unitários valiosos.
A divisão pragmática é rodar testes de arquitetura em um job de CI dedicado, ou marcá-los como integration tests e rodá-los depois que a suite rápida passar. A regra ainda bloqueia o merge. Ela apenas não deixa seu npm test local mais lento.
Falsos positivos acontecem quando a regra é muito ampla. Se você proibir todos os imports de node_modules na sua camada de domínio, você vai quebrar usos legítimos de date-fns ou zod. Regras precisam de exceções, e exceções precisam de manutenção. Uma regra com trinta entradas pathNot não está impondo arquitetura. Ela está codificando sua bagunça atual.
Eles podem dar falsa confiança. Passar um teste de arquitetura não significa que seu design é bom. Significa que seu design corresponde às regras que você escreveu. Se as regras estiverem erradas, os testes são apenas cargo culting automatizado.
Como adicioná-los sem quebrar o CI
Não comece com dez regras. Comece com uma. Escolha a direção de dependência que mais te causou dor. Talvez seja domain importando infrastructure. Talvez seja seu frontend importando código de backend diretamente.
Escreva o teste. Rode localmente. Conte as falhas. Se a contagem for zero, ou você é muito disciplinado ou a regra não está pegando o que você acha que está. Verifique com uma violação deliberada.
Se a contagem for cinquenta, você tem uma escolha. Corrija todas as cinquenta em um PR heroico, ou adicione exceções para violações existentes e proíba novas. A segunda opção é menos satisfatória e mais sustentável.
Faça o teste quebrar o CI. Não avisar. Quebrar. Um aviso é uma regra que engenheiros aprendem a ignorar.
FAQ
Testes de arquitetura substituem code review?
Não. Eles automatizam as partes do code review que humanos são ruins, como identificar imports transitivos através de vinte arquivos. Humanos ainda são melhores em julgar se uma nova dependência faz sentido.
E microservices?
Testes de arquitetura funcionam melhor dentro de uma única unidade deployável. Entre serviços, você impõe limites com contratos de API e isolamento de deployment, não com grafos de imports.
Devo testar convenções de nomenclatura?
Somente se inconsistência causar confusão real. Um teste que impõe sufixos Repository é útil em uma equipe de dez desenvolvedores. Provavelmente é ruído em um projeto solo.
Posso usar isso com monorepos?
Sim. Nx, Bazel e Turborepo todos permitem impor limites de module. Se você já usa um deles, use suas regras built-in. Elas rodam mais rápido e se integram com o grafo de dependências. Se não usa, um teste de arquitetura standalone é o ponto de entrada leve.
Comece com uma regra
Sua codebase já tem regras de arquitetura implícitas. Elas moram na cabeça do seu engenheiro sênior. Elas são impostas no code review quando esse engenheiro não está de férias. Elas são violadas às 23h antes de um prazo.
Escreva uma delas como teste. Deixe-o vermelho. Corrija as violações. Faça-o bloquear o CI.
Da próxima vez que alguém perguntar “Podemos importar infrastructure da camada de domain?”, a resposta não estará em uma wiki. Estará em um build quebrado.