Files
ersatztv/ErsatzTV.Architecture.Tests/LayeringTests.cs
T
timothyandClaude Opus 4.8 c788ae57f1
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 2m27s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 2m30s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m45s
test: enforce layer dependency direction with NetArchTest (#12)
Add ErsatzTV.Architecture.Tests asserting the solution's layering, so a
"just import it here" violation fails the build instead of eroding the
architecture over time:
- Core must not depend on Application/Infrastructure*/Scanner, nor on
  EF Core / Pomelo / Microsoft.Data.Sqlite / Dapper.
- FFmpeg (lowest layer) must not depend on any other ErsatzTV layer.
- Application must not depend on the concrete Infrastructure.Sqlite/.MySql
  providers (only the Infrastructure abstraction).
- Infrastructure must not depend on Application or the concrete providers.

Uses NetArchTest.eNhancedEdition — the maintained fork; the original
NetArchTest.Rules is unmaintained since 2021 and its older Mono.Cecil
doesn't reliably parse .NET 10 assemblies. Runs via the existing `dotnet
test` in CI. Rules verified with a negative control (a known-true
dependency asserted forbidden fails as expected — so they're not vacuous).

Refs #12

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 09:53:09 +02:00

105 lines
3.5 KiB
C#

using System.Reflection;
using NetArchTest.Rules;
using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Architecture.Tests;
// Enforces the dependency direction of the solution's layers (see docs/ci-cd.md / contributors guide).
// NetArchTest analyses the compiled assemblies, so a "just import it here" violation fails the build
// instead of eroding the architecture over time. Rules below reflect the intended layering and the
// current project-reference graph:
// FFmpeg (lowest) <- Core <- Infrastructure <- {Infrastructure.Sqlite, Infrastructure.MySql}
// Core <- Application; everything <- ErsatzTV (host / composition root)
[TestFixture]
public class LayeringTests
{
private static Assembly Core => Assembly.Load("ErsatzTV.Core");
private static Assembly FFmpeg => Assembly.Load("ErsatzTV.FFmpeg");
private static Assembly Application => Assembly.Load("ErsatzTV.Application");
private static Assembly Infrastructure => Assembly.Load("ErsatzTV.Infrastructure");
[Test]
public void Core_should_not_depend_on_outer_layers()
{
TestResult result = Types.InAssembly(Core)
.Should()
.NotHaveDependencyOnAny(
"ErsatzTV.Application",
"ErsatzTV.Infrastructure",
"ErsatzTV.Scanner")
.GetResult();
result.IsSuccessful.ShouldBeTrue(Describe(result));
}
[Test]
public void Core_should_not_depend_on_a_database_or_orm()
{
TestResult result = Types.InAssembly(Core)
.Should()
.NotHaveDependencyOnAny(
"Microsoft.EntityFrameworkCore",
"Pomelo.EntityFrameworkCore",
"Microsoft.Data.Sqlite",
"Dapper")
.GetResult();
result.IsSuccessful.ShouldBeTrue(Describe(result));
}
[Test]
public void FFmpeg_should_not_depend_on_other_layers()
{
TestResult result = Types.InAssembly(FFmpeg)
.Should()
.NotHaveDependencyOnAny(
"ErsatzTV.Core",
"ErsatzTV.Application",
"ErsatzTV.Infrastructure",
"ErsatzTV.Scanner")
.GetResult();
result.IsSuccessful.ShouldBeTrue(Describe(result));
}
[Test]
public void Application_should_not_depend_on_concrete_database_providers()
{
TestResult result = Types.InAssembly(Application)
.Should()
.NotHaveDependencyOnAny(
"ErsatzTV.Infrastructure.Sqlite",
"ErsatzTV.Infrastructure.MySql")
.GetResult();
result.IsSuccessful.ShouldBeTrue(Describe(result));
}
[Test]
public void Infrastructure_should_not_depend_on_application_or_concrete_providers()
{
TestResult result = Types.InAssembly(Infrastructure)
.Should()
.NotHaveDependencyOnAny(
"ErsatzTV.Application",
"ErsatzTV.Infrastructure.Sqlite",
"ErsatzTV.Infrastructure.MySql")
.GetResult();
result.IsSuccessful.ShouldBeTrue(Describe(result));
}
private static string Describe(TestResult result)
{
if (result.IsSuccessful)
{
return string.Empty;
}
IEnumerable<string> names = result.FailingTypes?.Select(t => t.FullName ?? t.Name)
?? Enumerable.Empty<string>();
return "Layering violation — offending types:\n " + string.Join("\n ", names);
}
}