NetArchTest.eNhancedEdition 1.4.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package NetArchTest.eNhancedEdition --version 1.4.0
NuGet\Install-Package NetArchTest.eNhancedEdition -Version 1.4.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NetArchTest.eNhancedEdition" Version="1.4.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add NetArchTest.eNhancedEdition --version 1.4.0
#r "nuget: NetArchTest.eNhancedEdition, 1.4.0"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install NetArchTest.eNhancedEdition as a Cake Addin
#addin nuget:?package=NetArchTest.eNhancedEdition&version=1.4.0

// Install NetArchTest.eNhancedEdition as a Cake Tool
#tool nuget:?package=NetArchTest.eNhancedEdition&version=1.4.0

net-workflow Nuget

NetArchTest.eNhancedEdition

A fluent API for .Net Standard that can enforce architectural rules in unit tests and create a self-testing architecture. Inspired by the ArchUnit library for Java.

NetArchTest.eNhancedEdition is based on NetArchTest v1.3.2. If you are not familiar with NetArchTest, you should start by reading introduction on Ben's blog.

Rationale

NetArchTest is well established mature library, but in order to push things forward, a few breaking changes had to be made, and that is how eNhancedEdition was born. eNhancedEdition uses almost identical Fluent API as a base library, but it is not 100% backward compatible, and it will never be.

What eNhancedEdition has to offer, that is not available in the NetArchTest v1.3.2:

  • fixed all know bugs present in v1.3.2 :
    • BenMorris/NetArchTest#98,
    • BenMorris/NetArchTest#101,
    • BenMorris/NetArchTest#120,
    • NeVeSpl/NetArchTest.eNhancedEdition#3
  • corrected design mistakes:
    • BenMorris/NetArchTest#119 - fixed nulls for Success result
    • BenMorris/NetArchTest#130 - for generic type, number of type parameters (e.g. `1) is no longer considered as a part of its name
  • added new features:
    • Slices
    • BenMorris/NetArchTest#67 - added rules: AreOfType, AreNotOfType
    • BenMorris/NetArchTest#97 - added rules: HaveSourceFileNameMatchingName, HaveSourceFilePathMatchingNamespace
    • BenMorris/NetArchTest#100 - added rules: AreImmutable, AreImmutableExternally, AreStateless
    • BenMorris/NetArchTest#104 - added rule: HaveMatchingTypeWithName
    • BenMorris/NetArchTest#105 - dependency search functions: HaveDependencyOnAny/OnlyHaveDependencyOn explain why a type fails test through IType.Explanation
    • BenMorris/NetArchTest#126 - added rules for structs, enums and delegates
    • BenMorris/NetArchTest#131 - added rules for all access modifiers: public, internal, private, protected, private protected, protected internal
    • BenMorris/NetArchTest#133 - added rules: AreInheritedByAnyType, AreNotInheritedByAnyType
    • added rules : AreUsedByAny, AreNotUsedByAny

Index

Getting started

The library is available as a package on NuGet: NetArchTest.eNhancedEdition.

Examples

[TestClass]
public class SampleApp_ModuleAlpha_Tests
{
    static readonly Assembly AssemblyUnderTest = typeof(TestUtils).Assembly;

    [TestMethod]
    public void PersistenceIsNotAccessibleFromOutsideOfModuleExceptOfDbContext()
    {
        var result = Types.InAssembly(AssemblyUnderTest)
                          .That()
                          .ResideInNamespace("SampleApp.ModuleAlpha.Persistence")
                          .And()
                          .DoNotHaveNameEndingWith("DbContext")
                          .Should()
                          .NotBePublic()
                          .GetResult();

        Assert.IsTrue(result.IsSuccessful);
    }

    [TestMethod]
    public void DomainIsIndependent()
    {
        var result = Types.InAssembly(AssemblyUnderTest)
                          .That()
                          .ResideInNamespace("SampleApp.ModuleAlpha.Domain")
                          .ShouldNot()
                          .HaveDependencyOtherThan(
                            "System",
                            "SampleApp.ModuleAlpha.Domain",
                            "SampleApp.SharedKernel.Domain",
                            "SampleApp.BuildingBlocks.Domain"
                          )
                          .GetResult();

        Assert.IsTrue(result.IsSuccessful, "Domain has lost its independence!");
    }

}

[TestClass]
public class SampleApp_ModuleOmega_Tests
{
    static readonly Assembly AssemblyUnderTest = typeof(TestUtils).Assembly;

    [TestMethod]
    public void RequestHandlersShouldBeSealed()
    {            
        var result = Types.InAssembly(AssemblyUnderTest)
                          .That()
                          .ImplementInterface(typeof(IRequestHandler<,>))
                          .Should()
                          .BeSealed()
                          .GetResult();

        Assert.IsTrue(result.IsSuccessful);
    }
}

Writing rules

The fluent API should direct you in building up a rule, based on a combination of predicates, conditions and conjunctions.

The starting point for any rule is the static Types class, where you load a set of types from a assembly, domain or patth.

var types = Types.InAssembly(typeof(MyClass).Assembly);

Once you have loaded the types, you can filter them using one or more predicates. These can be chained together using And() or Or() conjunctions:

types.That().ResideInNamespace("MyProject.Data");

Once the set of types have been filtered, you can apply a set of conditions using the Should() or ShouldNot() methods, e.g.

types.That().ResideInNamespace("MyProject.Data").Should().BeSealed();

Finally, you obtain a result from the rule by using an executor, i.e. use GetTypes() to return the types that match the rule or GetResult() to determine whether the rule has been met.

Note that GetResult() returns TestResult which contains a few lists of types:

  • LoadedTypes - all types loded by Types
  • SelectedTypesForTesting - types that passed predicates
  • FailingTypes- types that failed to meet the conditions
var result = types.That().ResideInNamespace("MyProject.Data").Should().BeSealed().GetResult();
var isValid = result.IsSuccessful;
var types = result.FailingTypes;

Dependency matrix:

type\has dependency on D1 D2 D3
a
b x
c x
d x x
e x
f x x
g x x
h x x x

Available rules:

Rule number<br> of required<br> dependencies <br>from the list type can have<br>a dependency<br>that is not<br>on the list passing types failing types
1 HaveDependencyOnAny(D1, D2) at least 1 yes c, d, e, f, g, h, a, b
2 HaveDependencyOnAll(D1, D2) all yes g, h a, b, c, d, e, f
3 OnlyHaveDependencyOn(D1, D2) >=0 no a, c, e, g b, d, f, h
1N NotHaveDependencyOnAny(D1, D2) none yes a, b c, d, e, f, g, h,
2N NotHaveDependencyOnAll(D1, D2) not all yes a, b, c, d, e, f g, h
3N HaveDependencyOtherThan(D1, D2) >=0 yes b, d, f, h, a, c, e, g

Explnation why a type fails dependecy search test is available on the failing type: IType.Explanation

Predicate number<br> of required<br> dependencies <br>from the list type can use<br>a type<br>that is not<br>on the list passing types failing types
R1 AreUsedByAny(c, d) at least 1 yes D2, D3 D1
R1N AreNotUsedByAny(c, d) none yes D1 D2, D3

Slices

var result = Types.InAssembly(typeof(ExampleDependency).Assembly)
                  .Slice()
                  .ByNamespacePrefix("MyApp.Features")
                  .Should()
                  .NotHaveDependenciesBetweenSlices()
                  .GetResult();

There is only one way, at least for now, to divide types into slices ByNamespacePrefix(string prefix) and it works as follows:

  1. Selects types which namespace starts with a given prefix, rest of the types are ignored.
  2. Slices are defined by the first part of the namespace that comes right after the prefix: namespacePrefix.(sliceName).restOfNamespace
  3. Types with the same sliceName part will be placed in the same slice. If sliceName is empty for a given type, the type will be also ignored (BaseFeature class from folowing image)

Slices

When we already have our types divided into slices, we can apply condition: NotHaveDependenciesBetweenSlices(). As the name suggest it detects if any dependency exists between slices. Dependency from slice to type that is not part of any other slice is allowed.

passing failing
Slices Slices

Custom rules

You can extend the library by writing custom rules that implement the ICustomRule interface. These can be applied as both predicates and conditions using a MeetsCustomRule() method, e.g.

var myRule = new CustomRule();

// Write your own custom rules that can be used as both predicates and conditions
var result = Types.InCurrentDomain()
    .That()
    .AreClasses()
    .Should()
    .MeetCustomRule(myRule)
    .GetResult()
    .IsSuccessful;

Options

User Options allows to configure how NetArchTest engine works.

var result = Types.InCurrentDomain()
    .That()
    .ResideInNamespace("NetArchTest.TestStructure.NameMatching.Namespace3")
    .Should()
    .HaveNameStartingWith("Some")
    .GetResult(Options.Default with { Comparer = StringComparison.Ordinal});

Assert.True(result.IsSuccessful);

Available options:

  • Comparer - allows to specify how strings will be compared (right now it only affects: Predicate.HaveName, Predicate.HaveNameStartingWith, Predicate.HaveNameEndingWith)

Limitations

NetArchTest is build on top of jbevain/cecil thus it works on CLI level. Unfortunately not every feature of C# language is represented in CLI, thus some things will never be available in NetArchTest, e.g.:

  • BenMorris/NetArchTest#81 - NetArchTest ignores a nameof expression
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.4.3 2,108 3/16/2024
1.4.2 1,202 3/11/2024
1.4.1 10,647 12/6/2023
1.4.0 302 11/27/2023
1.3.9 131 11/25/2023
1.3.8 711 11/21/2023
1.3.7 3,546 8/4/2023
1.3.6 1,853 6/1/2023
1.3.5 232 5/13/2023
1.3.4 5,692 1/11/2023
1.3.3 6,743 10/4/2022