Skip to content
Purview Dev logo

Getting started

Stable Architests Reviewed 2026-10-16 purview-dev/architests Star on GitHub architecture architecture-tests csharp dependency-rules dotnet nuget roslyn static-analysis tunit
dotnet add package Purview.Architecture.Testing

Add it to a test project that already references the projects you want to govern, or create a dedicated Architecture.UnitTests project. The test project must reference the production projects, directly or transitively, because the type-level rule reads their compiled assemblies from the test output.

Create .config/architecture-policy.json at the repository root. The smallest useful policy names each responsibility and what it may depend on:

{
"$schema": "https://raw.githubusercontent.com/purview-dev/architests/main/architecture-policy.schema.json",
"version": 2,
"sourceRoots": ["src"],
"responsibilities": [
{
"name": "Domain",
"category": "domain",
"purpose": "The domain model, its invariants and its rules.",
"mayDependOn": [],
"mustNotDependOn": ["Application", "Persistence", "Composition"]
},
{
"name": "Application",
"category": "application",
"purpose": "Coordinates the use cases and workflows.",
"mayDependOn": ["Domain"],
"mustNotDependOn": ["Persistence", "Composition"]
},
{
"name": "Persistence",
"category": "infrastructure",
"purpose": "Durable storage and persistence-specific configuration.",
"mayDependOn": ["Domain", "Application"],
"mustNotDependOn": ["Composition"]
},
{
"name": "Composition",
"category": "composition",
"purpose": "The composition root that selects and wires the implementations.",
"mayDependOn": ["Domain", "Application", "Persistence"],
"mustNotDependOn": []
}
]
}

A project name is <Responsibility>[.<Implementation>], and only the first segment is classified: Persistence.SqlServer and Persistence are both Persistence. Qualification is a naming choice, never a different set of rules.

sourceRoots defaults to ["src"]. Getting it wrong finds no projects, which the graphDiscovery rule reports rather than letting every other rule pass vacuously.

using Purview.Architecture.Testing;
[InheritsTests]
public sealed class ArchitectureTests : PurviewArchitectureTests;

That is the whole suite. Each rule is a separate [Test], so a failure names the rule rather than collapsing the set into one assertion.

[InheritsTests] is required. TUnit does not walk base types during discovery, so without the attribute the class compiles, the run is green, and nothing is enforced — the worst failure this package can have. TUnit’s analyzer reports TUnit0030; treat it as an error.

Terminal window
dotnet test

If graphDiscovery fails, sourceRoots is wrong. If naming fails, a project name does not map to a responsibility — add an alias or a waiver. If dependencyMatrix or assemblyDependencyMatrix fails, the message names the offending reference, the direction the dependency should run, and the likely fix.

The project-level matrix only sees declared ProjectReferences. To also catch a type that arrives transitively or through a package, enable the assembly check:

"assemblyRules": {
"enabled": true
}

An empty assemblyRoots scans the test output directory, which already holds every project the test project references. See Type-level rules for how an assembly is attributed to a project.

[InheritsTests]
public sealed class ArchitectureTests : PurviewArchitectureTests
{
protected override string? RepositoryRoot => "/path/to/repo";
}

RepositoryRoot defaults to probing upward from the test binary for the nearest policy file. The search stops at the repository boundary (.git) rather than continuing into parent folders — walking past it would happily find a sibling checkout’s policy and validate this repository against someone else’s rules.

SkipRule opts a whole repository out of a rule. Prefer a waiver: it is per project, carries a reason and a review date, and is itself checked.