Tu suite de tests verifica que calculateTotal devuelva 42 cuando recibe el input correcto. No verifica que src/domain/Invoice.ts pueda importar src/infrastructure/Database.ts. El compiler acepta ambos. Tus unit tests aceptan ambos. Pero uno de ellos es una violación de arquitectura que te costará una semana de refactorización dentro de seis meses.

Este es el punto ciego. Escribimos tests para la lógica y asumimos que la estructura se cuida sola. No es así.

Qué es en realidad un test de arquitectura

Un test de arquitectura es una aserción sobre la estructura de tu código, no sobre su comportamiento. Verifica que el grafo de dependencias coincida con el diseño que acordaste. Hace fallar el build cuando un desarrollador importa la capa incorrecta, crea una dependencia circular o nombra una clase de repository sin el sufijo que estandarizaste.

Estas no son reglas de lint. El linting detecta violaciones de estilo. Los tests de arquitectura detectan violaciones estructurales. La diferencia importa porque la estructura tiene semántica. Un module que importa a su padre no es un problema de estilo. Es un problema de diseño.

La mayoría de los equipos documentan estas reglas en una wiki, un README o un mensaje de Slack fijado por el tech lead. La documentación es útil para el onboarding. Es inútil para la aplicación. Un test de arquitectura traslada la regla de la memoria humana al pipeline de build, donde olvidarla cuesta un build rojo en lugar de un incident en producción.

Qué puedes aplicar con un test

El caso de uso obvio es la dirección de dependencias. Domain no debería importar infrastructure. UI no debería importar data access directamente. Estas reglas se mapean claramente a límites de packages o directorios.

Pero los tests de arquitectura pueden verificar más que imports. Aquí hay patrones que realmente importan en bases de código en producción.

Dependencias cíclicas. Un package que se importa a sí mismo a través de una cadena de otros tres packages sigue siendo cíclico. Tus ojos no lo detectarán en una code review. Un test que recorre el grafo de imports sí lo hará.

Convenciones de nomenclatura. Si tu equipo decidió que cada implementación de repository debe terminar en Repository, un test puede aplicar eso. Suena pedante hasta que alguien crea UserDao y UserRepo en la misma base de código y los nuevos ingenieros no saben cuál usar.

Dependencias prohibidas de librerías específicas. Tal vez tu capa de dominio no puede depender de axios, pg o fs. Tal vez tu frontend no puede importar lodash porque estás estandarizando en métodos nativos. Un test de arquitectura puede asegurar que un package específico nunca aparezca en el árbol de dependencias de un module específico.

Reglas de anotaciones y herencia. En Java, puedes testear que ninguna clase en ..domain.. use @Autowired. En C#, puedes testear que ninguna clase en Infrastructure implemente una interfaz de Application. Estas son restricciones estructurales que el análisis estático por sí solo no puede expresar sin conocimiento del dominio.

Cómo escribir uno

Los mejores tests de arquitectura parecen unit tests ordinarios. Se ejecutan en tu test runner existente. Aparecen en el mismo job de CI que tus otros tests. La única diferencia es sobre qué hacen la aserción.

Java con 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..");
}

Este es un test de JUnit. Se ejecuta con mvn test. Hace fallar el build. No se requiere un job especial de CI.

C# con 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 con import-linter:

Python no tiene un equivalente maduro de ArchUnit, pero import-linter te da una configuración declarativa que funciona como un test:

# .importlinter
[importlinter:contract:domain-independent]
name = Domain does not import infrastructure
type = forbidden
source_modules =
    myapp.domain
forbidden_modules =
    myapp.infrastructure

Ejecútalo con lint-imports en CI. Sale con código distinto de cero ante una violación.

Go: escribe el tuyo propio

Go no tiene una librería mainstream de testing de arquitectura. La mayoría de los equipos escriben un pequeño test que recorre el 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)
            }
        }
    }
}

Son veinte líneas. Vive en tu suite de tests. Se ejecuta en cada PR. Ese es el punto. No necesitas un framework. Necesitas una aserción.

Los trade-offs de los que nadie habla

Los tests de arquitectura no son gratis. Introducen una nueva categoría de fallo de build, y las nuevas categorías de fallo de build siempre generan fricción.

Son más lentos que los unit tests. Un test de arquitectura que escanea cada import en una base de código de 500.000 líneas toma tiempo. No horas. Segundos, a veces decenas de segundos. Pero eso es un orden de magnitud más lento que un unit test típico. Si los ejecutas en el mismo job que tus unit tests rápidos, pierdes el feedback loop que hace valioso el unit testing.

La división pragmática es ejecutar los tests de arquitectura en un job dedicado de CI, o etiquetarlos como integration tests y ejecutarlos después de que la suite rápida pase. La regla sigue bloqueando el merge. Simplemente no ralentiza tu equivalente local de npm test.

Los falsos positivos ocurren cuando la regla es demasiado amplia. Si prohibes todos los imports de node_modules en tu capa de dominio, romperás usos legítimos de date-fns o zod. Las reglas necesitan excepciones, y las excepciones necesitan mantenimiento. Una regla con treinta entradas pathNot no está aplicando arquitectura. Está codificando tu desorden actual.

Pueden dar una falsa confianza. Que un test de arquitectura pase no significa que tu diseño sea bueno. Significa que tu diseño coincide con las reglas que escribiste. Si las reglas están mal, los tests son solo cargo culting automatizado.

Cómo agregarlos sin romper CI

No empieces con diez reglas. Empieza con una. Elige la dirección de dependencias que más dolor te ha causado. Tal vez sea domain importando infrastructure. Tal vez sea tu frontend importando código de backend directamente.

Escribe el test. Ejecútalo localmente. Cuenta los fallos. Si la cuenta es cero, o eres muy disciplinado o la regla no está capturando lo que crees que captura. Verifícalo con una violación deliberada.

Si la cuenta es cincuenta, tienes una opción. Arregla las cincuenta en un PR heroico, o agrega excepciones para las violaciones existentes y prohíbe las nuevas. La segunda opción es menos satisfactoria y más sostenible.

Haz que el test haga fallar CI. No que advierta. Que falle. Una advertencia es una regla que los ingenieros aprenden a ignorar.

Preguntas frecuentes

¿Los tests de arquitectura reemplazan la code review?

No. Automatizan las partes de la code review en las que los humanos son malos, como detectar imports transitivos a través de veinte archivos. Los humanos siguen siendo mejores juzgando si una nueva dependencia tiene sentido.

¿Qué pasa con los microservicios?

Los tests de arquitectura funcionan mejor dentro de una unidad desplegable única. Entre servicios, aplicas los límites con contratos de API y aislamiento de despliegue, no con grafos de imports.

¿Debería testear las convenciones de nomenclatura?

Solo si la inconsistencia causa confusión real. Un test que aplica sufijos Repository es útil en un equipo de diez desarrolladores. Probablemente sea ruido en un proyecto individual.

¿Puedo usar esto con monorepos?

Sí. Nx, Bazel y Turborepo tienen aplicación de límites de module. Si ya estás usando uno, usa sus reglas integradas. Se ejecutan más rápido y se integran con el grafo de dependencias. Si no, un test de arquitectura independiente es el punto de entrada ligero.

Empieza con una regla

Tu base de código ya tiene reglas de arquitectura implícitas. Viven en la cabeza de tu ingeniero senior. Se aplican en la code review cuando ese ingeniero no está de vacaciones. Se violan a las 11 PM antes de una fecha límite.

Escribe una de ellas como un test. Haz que sea rojo. Arregla las violaciones. Haz que bloquee CI.

La próxima vez que alguien pregunte: “¿Podemos importar infrastructure desde la capa de domain?”, la respuesta no estará en una wiki. Estará en un build que falla.