O NetArchTest é uma pequena biblioteca .NET que transforma regras de arquitetura em testes unitários. Escrevemos um teste comum do xUnit ou do NUnit que verifica algo sobre os nossos próprios assemblies, por exemplo “nada no Domain referencia o Persistence” ou “toda classe do Persistence é selada”, e ele faz a compilação falhar quando alguém quebra a regra.

O autor, Ben Morris, abre o README do NetArchTest com uma única linha: “Uma API fluente para .Net Standard capaz de impor regras de arquitetura em testes unitários.” O README cita o ArchUnit, do Java, como inspiração. O NetArchTest é distribuído como o pacote NuGet NetArchTest.Rules, tem uma API fluente construída em torno de um único ponto de entrada, Types, e não exige executor de testes, arquivo de configuração nem mudanças nos projetos testados.

Para baixar o código-fonte deste artigo, você pode acessar nosso repositório no GitHub.

O que é o NetArchTest?

O NetArchTest é uma biblioteca .NET de código aberto para escrever testes de arquitetura: testes cujas asserções tratam da estrutura do nosso código, e não do comportamento dele.

Ele funciona lendo, com o Mono.Cecil, os assemblies compilados. Passamos um assembly para ele, filtramos um conjunto de tipos com That(), declaramos uma expectativa com Should() ou ShouldNot() e chamamos GetResult() para receber um resultado de aprovação ou reprovação, junto com a lista dos tipos que violam a regra.

Como o resultado é uma asserção booleana comum, esses testes ficam no mesmo projeto xUnit, NUnit ou MSTest que todo o resto e fazem a mesma compilação falhar. Não há executor separado nem configuração para manter.

O uso típico é proteger um design em camadas. A arquitetura Onion e a Clean Architecture dependem de uma regra de dependência que o compilador só impõe em parte, e uma referência de projeto adicionada às pressas pode invertê-la silenciosamente.

O README do NetArchTest diz o mesmo sobre o escopo: os testes de arquitetura “podem ser usados com qualquer framework de testes unitários e incorporados a um pipeline de compilação”.

Como criar a base para testes de arquitetura no .NET

Neste artigo, vamos usar um projeto simples baseado na arquitetura Onion:

Mostra a estrutura de projetos dos testes de arquitetura no .NET

Criamos vários projetos que representam as diferentes camadas desse padrão de arquitetura.

Se quiser saber mais sobre a arquitetura Onion, confira o nosso artigo sobre a arquitetura Onion no ASP.NET Core.

Primeiro, a camada de domínio é formada pelo projeto Domain. Já na camada de serviço, a implementação está dividida entre os projetos Services e Services.Abstractions. Na camada de infraestrutura, temos o projeto Persistence. Por fim, o projeto Api representa a camada de apresentação.

Vamos criar a nossa entidade principal:

public class Cat
{
    public required Guid Id { get; set; }
    public required string Name { get; set; }
    public required string Breed { get; set; }
    public required DateTime DateOfBirth { get; set; }
}

Aqui, criamos a classe Cat, em torno da qual a nossa aplicação é construída. Adicionamos, atualizamos, excluímos e consultamos gatos por meio do projeto Api.

Agora que os projetos estão prontos para os testes, vamos criar um novo projeto de teste xUnit e instalar o pacote NuGet necessário:

dotnet add package NetArchTest.Rules

Os testes também precisam do FluentAssertions (dotnet add package FluentAssertions --version 6.12.0), de referências aos projetos Persistence e Services e de diretivas using para FluentAssertions, NetArchTest.Rules, NetArchTest.Rules.Policies, Mono.Cecil, Persistence, Services.Abstractions, Domain.Entities, Domain.Exceptions e Domain.Exceptions.Base, que o exemplo mantém em GlobalUsings.cs.

Para saber mais sobre o framework em si, veja o nosso guia de testes unitários com xUnit no ASP.NET Core ou compare-o com as alternativas no artigo sobre as diferenças entre NUnit, xUnit e MSTest.

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Encontramos as principais regras para testes de arquitetura no pacote NetArchTest.Rules. O NetArchTest em si é uma biblioteca que nos permite criar testes que impõem convenções de design de classes, nomenclatura e dependências no .NET.

A biblioteca em si está estável desde 2021. O NetArchTest.Rules 1.3.2, publicado em 23 de maio de 2021, continua sendo a versão mais recente no nuget.org, e nada foi lançado desde então (consultado em 13 de setembro de 2026). O projeto continua compilando com o .NET moderno, e existe um fork mantido ativamente caso isso um dia se torne um impedimento: o NetArchTest.eNhancedEdition 1.4.5, com licença MIT, publicado em 4 de junho de 2025 (consultado em 13 de setembro de 2026).

Agora que está tudo pronto, vamos começar a testar!

Testes de arquitetura para impedir herança no .NET

Sabemos que queremos as classes da camada Persistence marcadas como sealed, o que garante que elas não possam ser herdadas. Vejamos como impor isso:

[Fact]
public void GivenPersistenceLayerClasses_WhenInheritanceIsAttempted_ThenItShouldNotBePossible()
{
    // Arrange
    var persistenceLayerAssembly = typeof(CatsDbContext).Assembly;

    // Act
    var result = Types.InAssembly(persistenceLayerAssembly)
        .Should().BeSealed()
        .GetResult();

    // Assert
    result.IsSuccessful.Should().BeTrue();
}

Primeiro, obtemos o assembly do projeto Persistence. Depois, começamos pela classe Types, que é o ponto de entrada da biblioteca NetArchTest.Rules. Em seguida, passamos a variável persistenceLayerAssembly ao método InAssembly(), que obtém todos os tipos desse assembly.

Em segundo lugar, chamamos o método Should(). Com ele, aplicamos condições à lista de tipos correspondentes que temos até aqui. No nosso caso, a única condição é que todas as classes sejam seladas, então usamos o método BeSealed(). Terminamos chamando o método GetResult().

Por fim, obtemos uma variável result do tipo TestResult. Ela tem uma propriedade booleana IsSuccessful, que indica se todos os tipos atendem ou não às nossas condições. Assim, com a biblioteca FluentAssertions, garantimos que a propriedade IsSuccessful retorne true.

O exemplo fixa o FluentAssertions na versão 6.12.0. A partir da versão 8, o pacote passou para a Xceed Community Licence e é pago para uso comercial, enquanto a versão 7 continua sob a Apache-2.0. Por isso, fixamos a dependência de propósito, em vez de pegar a versão mais recente às cegas (FluentAssertions no nuget.org, metadados de licença das versões 8.10.0 e 7.2.0 consultados em 13 de setembro de 2026). Se preferirmos evitar a questão de vez, Assert.True(result.IsSuccessful) funciona da mesma forma, sem nenhuma dependência extra, ou então o Shouldly, para asserções mais legíveis, é uma alternativa gratuita que vale a pena conhecer.

O FluentAssertions é uma ótima forma de melhorar os seus testes unitários. Confira o nosso artigo sobre como melhorar testes unitários com o FluentAssertions.

Testes de arquitetura que impõem a visibilidade das classes no .NET

Na arquitetura Onion, as implementações concretas dos serviços não devem ficar visíveis fora da camada de serviço.

Isso não vale para as interfaces do projeto Services.Abstractions, que devem ser todas públicas, então vamos criar um teste para isso:

[Fact]
public void GivenServiceLayerInterfaces_WhenAccessedFromOtherProjects_ThenTheyAreVisible()
{
    // Arrange
    var serviceLayerAssembly = typeof(IServiceManager).Assembly;

    // Act
    var result = Types.InAssembly(serviceLayerAssembly)
        .Should().BePublic()
        .GetResult();

    // Assert
    result.IsSuccessful.Should().BeTrue();
}

Com o tipo IServiceManager, obtemos o assembly em que ficam todos os tipos de interface da camada de serviço. Depois, usamos o método BePublic(), que é a condição que buscamos. No fim, verificamos que o resultado é bem-sucedido.

Testes de arquitetura que impõem a implementação de interfaces no .NET

Podemos garantir facilmente que as nossas classes implementem as interfaces corretas ou herdem das classes desejadas:

[Fact]
public void GivenCatNotFoundException_ThenShouldInheritFromNotFoundException()
{
    // Arrange
    var domainLayerAssembly = typeof(CatNotFoundException).Assembly;

    // Act
    var result = Types.InAssembly(domainLayerAssembly)
        .That().ResideInNamespace("Domain.Exceptions")
        .And().HaveNameStartingWith("Cat")
        .Should().Inherit(typeof(NotFoundException))
        .GetResult();

    // Assert
    result.IsSuccessful.Should().BeTrue();
}

Aqui, criamos um teste para garantir que a classe CatNotFoundException herde da classe NotFoundException. Depois de especificar o assembly, usamos o método That() para indicar a quais tipos as nossas condições se aplicam. Usamos o método ResideInNamespace() para obter todos os tipos do namespace Domain.Exceptions.

Isso também pega os tipos de qualquer namespace filho, então adicionamos o método And() para filtrar mais e continuamos com o método HaveNameStartingWith(). Só a nossa classe CatNotFoundException passa por todos os filtros, e seguimos com os métodos Should() e Inherit(). Por fim, verificamos que o resultado é true, o que confirma que as nossas regras de herança são respeitadas.

Testes de arquitetura que impõem as dependências entre projetos no .NET

A arquitetura Onion e a Clean Architecture impõem regras rígidas às dependências entre projetos. Vamos criar um teste para verificar essas regras:

[Fact]
public void GivenDomainLayer_ThenShouldNotHaveAnyDependencies()
{
    // Arrange
    var domainLayerAssembly = typeof(Cat).Assembly;

    // Act
    var result = Types.InAssembly(domainLayerAssembly)
        .ShouldNot().HaveDependencyOnAny(
            ["Api", "Contracts", "Persistence", "Services", "Services.Abstractions"])
        .GetResult();

    // Assert
    result.IsSuccessful.Should().BeTrue();
}

Primeiro, escrevemos um teste para garantir que o projeto Domain não tenha nenhuma dependência dos demais projetos da solução. Depois, obtemos o assembly necessário e usamos o método ShouldNot() junto com o método HaveDependencyOnAny(). A este último, passamos todos os outros namespaces como strings, usando expressões de coleção.

Observe que muitos dos métodos condicionais de NetArchTest.Rules têm duas versões, uma que começa com Have e outra com NotHave. Isso, junto com os métodos Should() e ShouldNot(), permite escrever os testes de duas formas diferentes, com o mesmo resultado.

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Se a divisão em camadas não serve para todas as equipes, a Vertical Slice Architecture no ASP.NET Core organiza o código por funcionalidade, em vez de por camada, e a mesma abordagem do NetArchTest continua valendo para impor as fronteiras dela.

Como escrever uma regra personalizada para testes de arquitetura no .NET

A biblioteca NetArchTest.Rules também nos permite criar regras personalizadas:

public class CustomServiceLayerRule : ICustomRule
{
    public bool MeetsRule(TypeDefinition type)
        => type.IsInterface && type.IsPublic && type.Name.StartsWith('I');
}

Para criar uma regra personalizada, precisamos implementar a interface ICustomRule e o método MeetsRule() dela. Criamos uma regra para garantir que todos os tipos do projeto Services.Abstractions sejam interfaces, tenham o modificador de acesso public e tenham o prefixo I no nome.

Agora, vamos usar a nossa regra:

[Fact]
public void GivenServiceInterfaces_ThenShouldBePublicAndBeInterfacesAndStartWithI()
{
    // Arrange
    var serviceLayerAssembly = typeof(IServiceManager).Assembly;
    var myCustomRule = new CustomServiceLayerRule();

    // Act
    var result = Types.InAssembly(serviceLayerAssembly)
        .Should().MeetCustomRule(myCustomRule)
        .GetResult();

    // Assert
    result.IsSuccessful.Should().BeTrue();
}

No teste, primeiro criamos uma instância da classe CustomServiceLayerRule. Depois, passamos essa instância ao método MeetCustomRule(). Isso aplica a regra a todos os tipos correspondentes, que, no nosso caso, são todos os tipos do projeto Services.Abstractions.

Como definir políticas de regras personalizadas para testes de arquitetura no .NET

A biblioteca NetArchTest.Rules permite usar políticas personalizadas que combinam várias regras diferentes:

[Fact]
public void GivenServiceInterfaces_ThenShouldMeetCustomRuleAndServiceManagerShouldNotHaveDependencyOnContracts()
{
    // Arrange
    var serviceLayerAssembly = typeof(IServiceManager).Assembly;
    var myCustomRule = new CustomServiceLayerRule();

    var customPolicy = Policy.Define(
            "Service Interfaces Policy",
            "This policy ensures that all types meet the given conditions")
        .For(Types.InAssembly(serviceLayerAssembly))
        .Add(types => types
            .Should().MeetCustomRule(myCustomRule))
        .Add(types => types
            .That().HaveNameEndingWith("Manager")
            .ShouldNot().HaveDependencyOn("Contracts")
        );

    // Act
    var results = customPolicy.Evaluate();

    // Assert
    foreach (var result in results.Results)
    {
        result.IsSuccessful.Should().BeTrue();
    }
}

Primeiro, começamos a criar uma política personalizada chamando o método Define() no tipo Policy. O método recebe dois parâmetros: um nome e uma descrição. Continuamos a criação da política com o método For(), em que especificamos quais tipos vamos testar.

Em seguida, passamos a definir conjuntos de regras para diferentes subconjuntos de tipos. Fazemos isso encadeando chamadas ao método Add(), nas quais filtramos os tipos e usamos os métodos Should() e ShouldNot() para verificar as condições desejadas.

Com a política pronta, chamamos o método Evaluate() para obter um objeto PolicyResults que contém uma lista de objetos PolicyResult. Por fim, verificamos que a propriedade IsSuccessful de cada resultado é true, o que garante que todas as condições foram atendidas.

Queremos imporMétodo da regraExemplo
As classes não podem ser herdadasBeSealed()Types.InAssembly(a).Should().BeSealed()
Os tipos permanecem internosNotBePublic().That().ResideInNamespace("X.Internal").Should().NotBePublic()
Uma camada não depende de outraNotHaveDependencyOn().That().ResideInNamespace("Domain").ShouldNot().HaveDependencyOn("Persistence")
Os tipos implementam uma interfaceImplementInterface().That().HaveNameEndingWith("Repository").Should().ImplementInterface(typeof(IRepository))
Convenção de nomenclaturaHaveNameEndingWith().That().Inherit(typeof(ControllerBase)).Should().HaveNameEndingWith("Controller")
Os tipos ficam no namespace certoResideInNamespace().That().ImplementInterface(typeof(IHandler)).Should().ResideInNamespace("Application")
Várias regras como uma só unidadePolicy.Define()Policy.Define("Layering", "...").For(Types.InAssembly(a)).Add(...)

Por que escrever testes de arquitetura no .NET?

As decisões de arquitetura se deterioram em silêncio. A regra de dependência com que todos concordaram no início do projeto sobrevive exatamente enquanto alguém se lembra de verificá-la na revisão de código.

Um teste de arquitetura tira essa verificação da atenção humana e a coloca na compilação. Usar um tipo do Contracts no Domain deixa de ser um comentário de revisão que alguém pode deixar passar e vira um teste vermelho, com o tipo que viola a regra listado no TestResult do teste.

O segundo benefício é a integração de novos desenvolvedores. Um desenvolvedor novo que lê ShouldNot().HaveDependencyOn("Persistence") aprende a regra em uma linha, sem precisar encontrar o registro de decisão de arquitetura que a explica.

O custo é real, mas pequeno: esses testes rodam sobre assemblies compilados, então precisam de uma compilação antes, e uma regra rígida demais vira um incômodo que as pessoas desativam em vez de cumprir.

A regra prática é codificar as restrições pelas quais realmente rejeitaríamos um pull request (camadas e nomenclatura) e deixar as questões de gosto para a revisão.

Conclusão

Neste artigo, usamos o NetArchTest.Rules para escrever testes de arquitetura para aplicações .NET. Impusemos convenções de design de classes, nomenclatura e dependências: bloqueamos heranças indesejadas, verificamos as dependências entre projetos e adicionamos regras personalizadas próprias. Também agrupamos regras em políticas, o que permite moldar uma suíte de testes em torno das restrições de um projeto. Com esses testes na compilação, a arquitetura que combinamos continua sendo a arquitetura que entregamos.

Testado com .NET 10.0.10.