Skip to content

Commit

Permalink
Added architecture tests for horizontal layers and vertical slices. #10
Browse files Browse the repository at this point in the history
  • Loading branch information
jezzsantos committed Mar 20, 2024
1 parent 5b96dbe commit 413fb8f
Show file tree
Hide file tree
Showing 13 changed files with 316 additions and 10 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
run: >
dotnet test --no-build --verbosity normal
--configuration ${{env.TESTINGONLY_BUILD_CONFIGURATION}}
--filter:Category="Unit" --collect:"XPlat Code Coverage" --results-directory:"src/TestResults/csharp"
--filter:"Category=Unit|Category=Unit.Architecture" --collect:"XPlat Code Coverage" --results-directory:"src/TestResults/csharp"
--logger:"trx"
--logger:"junit;LogFileName={assembly}.junit.xml;MethodFormat=Class;FailureBodyFormat=Verbose"
--test-adapter-path:. "${{env.SOLUTION_PATH}}"
Expand All @@ -61,7 +61,7 @@ jobs:
run: >
dotnet test --no-build --verbosity normal
--configuration ${{env.TESTINGONLY_BUILD_CONFIGURATION}}
--filter:Category="Integration.Web" --collect:"XPlat Code Coverage" --results-directory:"src/TestResults/csharp"
--filter:"Category=Integration.Web" --collect:"XPlat Code Coverage" --results-directory:"src/TestResults/csharp"
--logger:"trx"
--logger:"junit;LogFileName={assembly}.junit.xml;MethodFormat=Class;FailureBodyFormat=Verbose"
--test-adapter-path:. "${{env.SOLUTION_PATH}}"
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ There are 3 flavors of build configuration: `Debug`, `Release` and `ReleaseForDe
## Testing the code

* Run all the **unit** tests (`Category=Unit`)
* Run all the **architecture** tests (`Category=Unit.Architecture`)
* Run all the **integration** tests (`Category=Integration.Web`)

> All automated tests must pass in GitHub Actions to submit changes to the codebase
Expand Down
16 changes: 8 additions & 8 deletions README_DERIVATIVE.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,15 @@ When pushed, all branches will be built and tested with GitHub actions
2. Run these tests:

In Rider, run all C# tests with Category= `Unit` and `Integration.Web`
In Rider, run all C# tests with Category= `Unit`, `Unit.Architecture` and `Integration.Web`

> Note: Use `Group By > Category` in Rider's unit test explorer to view these three categories easily.
OR, in a terminal:

- `dotnet test --filter:"Category=Unit" src\SaaStack.sln`
- `dotnet test --filter:"Category=Unit|Category=Unit.Architecture" src\SaaStack.sln`

- `dotnet test --filter Category=Integration.Web src\SaaStack.sln`
- `dotnet test --filter:"Category=Integration.Web" src\SaaStack.sln`

3. Configure your "Commit " window to select the "Cleanup with 'Full Cleanup' profile".

Expand Down Expand Up @@ -268,13 +268,13 @@ To kill these processes:

### Everyday tests

Run all C# tests with Category= `Unit` and `Integration.Web`
Run all C# tests with Category= `Unit`, `Unit.Architecture` and `Integration.Web`

OR, in a terminal:

- `dotnet test --filter:"Category=Unit" src\SaaStack.sln`
- `dotnet test --filter:"Category=Unit|Category=Unit.Architecture" src\SaaStack.sln`

- `dotnet test --filter Category=Integration.Web src\SaaStack.sln`
- `dotnet test --filter:"Category=Integration.Web" src\SaaStack.sln`

> Note: All tests will be run in parallel in `Rider` or in `dotnet test`.
Expand All @@ -290,7 +290,7 @@ These tests should NOT be run frequently and can be scheduled to run as part of
>
> Warning: They may incur charges, or they may trigger rate-limiting policies on the accounts they are run against.
`dotnet test --filter Category=Integration.Persistence src\SaaStack.sln` (requires installing the server persistence components listed at the top of this page)
`dotnet test --filter:"Category=Integration.Persistence" src\SaaStack.sln` (requires installing the server persistence components listed at the top of this page)

> Note: If any of the `Integration.Persistence` category of tests fail, it is likely due to the fact that you don't have that technology installed on your local machine, or that you are not running your IDE as Administrator, and therefore cannot start/stop those local services without elevated permissions.
Expand All @@ -306,7 +306,7 @@ Only run these kinds of tests when the code in the technology adapters changes.
>
> Warning: They may incur charges, or they may trigger rate-limiting policies on the accounts they are run against.
`dotnet test --filter Category=Integration.External src\SaaStack.sln` (requires internet access to external services)
`dotnet test --filter:"Category=Integration.External" src\SaaStack.sln` (requires internet access to external services)

# Versioning the Code

Expand Down
17 changes: 17 additions & 0 deletions src/ApiHost1.ArchitectureTests/ApiHost1.ArchitectureTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\ApiHost1\ApiHost1.csproj" />
<ProjectReference Include="..\ArchitectureTesting.Common\ArchitectureTesting.Common.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
</ItemGroup>

</Project>
15 changes: 15 additions & 0 deletions src/ApiHost1.ArchitectureTests/HorizontalLayersSpec.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using ArchitectureTesting.Common;
using Xunit;

namespace ApiHost1.ArchitectureTests;

[Trait("Category", "Unit.Architecture")]
[Collection("Architecture")]
public class HorizontalLayersSpec : HorizontalLayersSpecBase<Program>
{
public HorizontalLayersSpec(ArchitectureSpecSetup<Program> setup) : base(setup)
{
}

//TODO: add additional tests here
}
7 changes: 7 additions & 0 deletions src/ApiHost1.ArchitectureTests/Setup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using ArchitectureTesting.Common;
using Xunit;

namespace ApiHost1.ArchitectureTests;

[CollectionDefinition("Architecture", DisableParallelization = true)]
public class AllArchitectureSpecs : ICollectionFixture<ArchitectureSpecSetup<Program>>;
15 changes: 15 additions & 0 deletions src/ApiHost1.ArchitectureTests/VerticalSlicesSpec.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using ArchitectureTesting.Common;
using Xunit;

namespace ApiHost1.ArchitectureTests;

[Trait("Category", "Unit.Architecture")]
[Collection("Architecture")]
public class VerticalSlicesSpec : VerticalSlicesSpecBase<Program>
{
public VerticalSlicesSpec(ArchitectureSpecSetup<Program> setup) : base(setup)
{
}

//TODO: add additional tests here
}
43 changes: 43 additions & 0 deletions src/ArchitectureTesting.Common/ArchitectureSpecSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using ArchUnitNET.Domain;
using ArchUnitNET.Fluent;
using ArchUnitNET.Fluent.Syntax.Elements.Types;
using ArchUnitNET.Loader;

namespace ArchitectureTesting.Common;

// ReSharper disable once ClassNeverInstantiated.Global
public class ArchitectureSpecSetup<THost>
{
/// <summary>
/// Provides a base class for all architecture tests, that works on all the assemblies that are loaded and reachable
/// from the specified <see cref="THost" />.
/// Note: We must NOT cache the properties like <see cref="DomainLayer" /> between tests.
/// </summary>
public ArchitectureSpecSetup()
{
var hostAssembly = typeof(THost).Assembly;
var assemblyLocation = Path.GetDirectoryName(hostAssembly.Location);

Architecture = new ArchLoader()
.LoadFilteredDirectory(assemblyLocation, "*.dll")
.Build();
}

public GivenTypesConjunctionWithDescription ApplicationLayer => ArchRuleDefinition.Types(true).That()
.ResideInNamespace(ArchitectureTestingConstants.Layers.Application.SubdomainProjectNamespaces, true)
.Or().ResideInNamespace(ArchitectureTestingConstants.Layers.Application.PlatformProjectNamespaces, true)
.As(ArchitectureTestingConstants.Layers.Application.DisplayName);

public Architecture Architecture { get; }

public GivenTypesConjunctionWithDescription DomainLayer => ArchRuleDefinition.Types(true).That()
.ResideInNamespace(ArchitectureTestingConstants.Layers.Domain.SubdomainProjectNamespaces, true)
.Or().ResideInNamespace(ArchitectureTestingConstants.Layers.Domain.PlatformProjectNamespaces, true)
.As(ArchitectureTestingConstants.Layers.Domain.DisplayName);

public GivenTypesConjunctionWithDescription InfrastructureLayer => ArchRuleDefinition.Types(true).That()
.ResideInNamespace(ArchitectureTestingConstants.Layers.Infrastructure.SubdomainProjectNamespaces, true)
.Or().ResideInNamespace(ArchitectureTestingConstants.Layers.Infrastructure.PlatformProjectsNamespaces, true)
.Or().ResideInNamespace(ArchitectureTestingConstants.Layers.Infrastructure.ApiHostProjectsNamespaces, true)
.As(ArchitectureTestingConstants.Layers.Infrastructure.DisplayName);
}
18 changes: 18 additions & 0 deletions src/ArchitectureTesting.Common/ArchitectureTesting.Common.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\UnitTesting.Common\UnitTesting.Common.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="TngTech.ArchUnitNET" Version="0.10.5" />
<PackageReference Include="TngTech.ArchUnitNET.xUnit" Version="0.10.5" />
</ItemGroup>

</Project>
35 changes: 35 additions & 0 deletions src/ArchitectureTesting.Common/ArchitectureTestingConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace ArchitectureTesting.Common;

public static class ArchitectureTestingConstants
{
public static class Layers
{
public static class Domain
{
public const string AllOthersLabel = "Other Subdomains' Domain Layer types";
public const string DisplayName = "Domain Layer";
public const string PlatformProjectNamespaces = $@"^{ProjectSuffix}[\w\.]*$";
public const string ProjectSuffix = "Domain";
public const string SubdomainProjectNamespaces = $@"^[\w]+{ProjectSuffix}[\w\.]*$";
}

public static class Application
{
public const string AllOthersLabel = "Other Subdomains' Application Layer types";
public const string DisplayName = "Application Layer";
public const string PlatformProjectNamespaces = $@"^{ProjectSuffix}[\w\.]*$";
public const string ProjectSuffix = "Application";
public const string SubdomainProjectNamespaces = $@"^[\w]+{ProjectSuffix}[\w\.]*$";
}

public static class Infrastructure
{
public const string AllOthersLabel = "Other Subdomains' Infrastructure Layer types";
public const string ApiHostProjectsNamespaces = @"^Api[\w]+Host[\w\.]*$";
public const string DisplayName = "Infrastructure Layer";
public const string PlatformProjectsNamespaces = $@"^{ProjectSuffix}[\w\.]*$";
public const string ProjectSuffix = "Infrastructure";
public const string SubdomainProjectNamespaces = $@"^[\w]+{ProjectSuffix}[\w\.]*$";
}
}
}
42 changes: 42 additions & 0 deletions src/ArchitectureTesting.Common/HorizontalLayersSpecBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using ArchUnitNET.Fluent;
using ArchUnitNET.xUnit;
using Xunit;

namespace ArchitectureTesting.Common;

/// <summary>
/// Provides a base class for testing the horizontal layers of a specific <see cref="THost" />
/// </summary>
public abstract class HorizontalLayersSpecBase<THost>
{
protected readonly ArchitectureSpecSetup<THost> Setup;

protected HorizontalLayersSpecBase(ArchitectureSpecSetup<THost> setup)
{
Setup = setup;
}

[Fact]
public void WhenAnyDomainLayerTypeDependsOnAnyApplicationLayerType_ThenFails()
{
ArchRuleDefinition.Types().That().Are(Setup.DomainLayer)
.Should().NotDependOnAny(Setup.ApplicationLayer)
.Check(Setup.Architecture);
}

[Fact]
public void WhenAnyDomainLayerTypeDependsOnAnyInfrastructureLayerType_ThenFails()
{
ArchRuleDefinition.Types().That().Are(Setup.DomainLayer)
.Should().NotDependOnAny(Setup.InfrastructureLayer)
.Check(Setup.Architecture);
}

[Fact]
public void WhenAnyApplicationLayerTypeDependsOnAnyInfrastructureLayerType_ThenFails()
{
ArchRuleDefinition.Types().That().Are(Setup.ApplicationLayer)
.Should().NotDependOnAny(Setup.InfrastructureLayer)
.Check(Setup.Architecture);
}
}
95 changes: 95 additions & 0 deletions src/ArchitectureTesting.Common/VerticalSlicesSpecBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using ArchUnitNET.Domain.Extensions;
using ArchUnitNET.Fluent;
using ArchUnitNET.xUnit;
using Xunit;

namespace ArchitectureTesting.Common;

/// <summary>
/// Provides a base class for testing each vertical slice (subdomain) of a specific <see cref="THost" />
/// </summary>
public abstract class VerticalSlicesSpecBase<THost>
{
private readonly List<string> _allSubdomainTypes;
private readonly Func<string, string, string> _otherSubdomainProjectsNamespaceFormat =
(domainName, prefix) => $@"^(((?!{domainName})[\w]+){prefix}((\.[\w]+)*))$";
private readonly Func<string, string, string> _subdomainProjectNamespaceFormat =
(domainName, prefix) => $@"^{domainName}{prefix}((\.[\w]+)*)$";
protected readonly ArchitectureSpecSetup<THost> Setup;

protected VerticalSlicesSpecBase(ArchitectureSpecSetup<THost> setup)
{
Setup = setup;
_allSubdomainTypes = Setup.Architecture.Namespaces
.Where(ns => ns.NameEndsWith(ArchitectureTestingConstants.Layers.Domain.ProjectSuffix))
.Select(ns =>
ns.Name.Substring(0,
ns.Name.IndexOf(ArchitectureTestingConstants.Layers.Domain.ProjectSuffix,
StringComparison.Ordinal)))
.Distinct()
.ToList();
}

[Fact]
public void WhenAnyDomainTypeDependsOnAnotherSubdomainDomainType_ThenFails()
{
_allSubdomainTypes.ForEach(domainName =>
{
var subdomainTypes = ArchRuleDefinition.Types().That()
.ResideInNamespace(
_subdomainProjectNamespaceFormat(domainName,
ArchitectureTestingConstants.Layers.Domain.ProjectSuffix), true)
.As($"{domainName}{ArchitectureTestingConstants.Layers.Domain.ProjectSuffix}");
var anyOtherSubdomainType = ArchRuleDefinition.Types().That()
.ResideInNamespace(_otherSubdomainProjectsNamespaceFormat(domainName,
ArchitectureTestingConstants.Layers.Domain.ProjectSuffix), true)
.As(ArchitectureTestingConstants.Layers.Domain.AllOthersLabel);

ArchRuleDefinition.Types().That().Are(subdomainTypes)
.Should().NotDependOnAny(anyOtherSubdomainType)
.Check(Setup.Architecture);
});
}

[Fact]
public void WhenAnyApplicationTypeDependsOnAnotherSubdomainApplicationType_ThenFails()
{
_allSubdomainTypes.ForEach(domainName =>
{
var subdomainTypes = ArchRuleDefinition.Types().That()
.ResideInNamespace(
_subdomainProjectNamespaceFormat(domainName,
ArchitectureTestingConstants.Layers.Application.ProjectSuffix), true)
.As($"{domainName}{ArchitectureTestingConstants.Layers.Application.ProjectSuffix}");
var anyOtherSubdomainType = ArchRuleDefinition.Types().That()
.ResideInNamespace(_otherSubdomainProjectsNamespaceFormat(domainName,
ArchitectureTestingConstants.Layers.Application.ProjectSuffix), true)
.As(ArchitectureTestingConstants.Layers.Application.AllOthersLabel);

ArchRuleDefinition.Types().That().Are(subdomainTypes)
.Should().NotDependOnAny(anyOtherSubdomainType)
.Check(Setup.Architecture);
});
}

[Fact]
public void WhenAnyInfrastructureTypeDependsOnAnotherSubdomainInfrastructureType_ThenFails()
{
_allSubdomainTypes.ForEach(domainName =>
{
var subdomainTypes = ArchRuleDefinition.Types().That()
.ResideInNamespace(
_subdomainProjectNamespaceFormat(domainName,
ArchitectureTestingConstants.Layers.Infrastructure.ProjectSuffix), true)
.As($"{domainName}{ArchitectureTestingConstants.Layers.Infrastructure.ProjectSuffix}");
var anyOtherSubdomainType = ArchRuleDefinition.Types().That()
.ResideInNamespace(_otherSubdomainProjectsNamespaceFormat(domainName,
ArchitectureTestingConstants.Layers.Infrastructure.ProjectSuffix), true)
.As(ArchitectureTestingConstants.Layers.Infrastructure.AllOthersLabel);

ArchRuleDefinition.Types().That().Are(subdomainTypes)
.Should().NotDependOnAny(anyOtherSubdomainType)
.Check(Setup.Architecture);
});
}
}
Loading

0 comments on commit 413fb8f

Please sign in to comment.