A lightweight, opinionated support library for writing clean unit tests in .NET.
The goal of this package is not to replace your test framework, assertion library, or mocking library. It provides a small set of helpers that keep common unit test setup readable, consistent, and low-noise.
Library.UnitTesting is intentionally boring in the places where tests should be boring.
Library.UnitTesting is built around a pragmatic testing stack:
- NUnit for test execution
- Shouldly for readable assertions
- Moq for mocks
- AutoFixture for creating anonymous test data
- A small
UnitTestBase<TTarget>base class for shared unit test setup - AutoFixture-backed test data creation through
Instantiator ItExthelpers for expressive Moq argument matching- Explicit null guard policy assertions
- Explicit async-void policy assertions
The important word is explicit.
Older versions of this library leaned more heavily into automatic policy checks from the test base class. Modern usage keeps policy checks local, intentional, and visible in the test that cares about them.
dotnet add package Library.UnitTestingOr install it through the NuGet package manager in your IDE.
Given a service like this:
namespace SampleApp;
using System.Globalization;
using SampleApp.Contracts;
public class SampleService(IDateTimeProvider dateTimeProvider) : ISampleService
{
private readonly IDateTimeProvider dateTimeProvider = dateTimeProvider ?? throw new ArgumentNullException(nameof(dateTimeProvider));
public string Greet(string name)
{
ArgumentNullException.ThrowIfNull(name);
var now = this.dateTimeProvider.Now;
return string.Create(
CultureInfo.InvariantCulture,
$"Hello, {name}. It's {now:HH:mm}.");
}
}A test can focus on the behavior instead of repeated setup noise:
namespace SampleAppUnitTests;
using System.Globalization;
using Library.UnitTesting.Common;
using Moq;
using NUnit.Framework;
using SampleApp;
using SampleApp.Contracts;
using Shouldly;
[TestFixture]
public class SampleServiceTests : UnitTestBase<SampleService>
{
private readonly Mock<IDateTimeProvider> dateTimeProviderMock = new();
[Test]
public void Greet_Valid_ReturnsCorrectResult()
{
// Arrange
var name = this.Instantiator.Random<string>();
var dateTime = this.Instantiator.Random<DateTime>();
var expected = string.Create(
CultureInfo.InvariantCulture,
$"Hello, {name}. It's {dateTime:HH:mm}.");
this.dateTimeProviderMock.Setup(x => x.Now).Returns(dateTime);
var target = this.GetTarget();
// Act
var actual = target.Greet(name);
// Assert
actual.ShouldBe(expected);
}
protected override void Setup()
{
this.dateTimeProviderMock.Reset();
}
protected override SampleService GetTarget()
{
return new SampleService(this.dateTimeProviderMock.Object);
}
}UnitTestBase<TTarget> gives each test a fresh Instantiator and a virtual setup hook.
[TestFixture]
public class MyServiceTests : UnitTestBase<MyService>
{
[Test]
public void DoWork_ValidInput_ReturnsExpectedResult()
{
var input = this.Instantiator.Random<MyInput>();
var target = this.GetTarget();
var actual = target.DoWork(input);
actual.ShouldNotBeNull();
}
protected override void Setup()
{
// Optional per-test setup.
}
protected override MyService GetTarget()
{
return new MyService();
}
}The base class deliberately does not auto-run policy tests. Policy checks should be explicit tests.
Use the fixture-backed instantiator when the actual values do not matter.
[Test]
public void Save_ValidMessage_StoresMessage()
{
var message = this.Instantiator.Random<Message>();
var target = this.GetTarget();
target.Save(message);
this.messageRepositoryMock
.Verify(x => x.Save(message), Times.Once);
}For many tests, this keeps the arrange phase small and avoids hand-building object trees that are irrelevant to the behavior under test.
You can also create multiple values:
var messages = this.Instantiator.Random<Message>(5);And register custom creation functions:
this.Instantiator.RegisterCreationFunction(() => new CustomerId(42));RandomString creates a random string with the requested length.
var value = this.Instantiator.RandomString(32);ItExt exists to make common Moq argument checks more readable.
this.messageRepositoryMock
.Verify(x => x.Save(ItExt.IsEquivalent(expectedMessage)));For date/time comparisons, use IsCloseTo:
this.clockMock
.Verify(x => x.Schedule(ItExt.IsCloseTo(expectedTime, TimeSpan.FromSeconds(1))));Use these helpers for clarity, not cleverness.
If a plain It.Is<T> is clearer, use the plain Moq API.
Null guard checks are useful, but they are also easy to overdo.
Prefer normal language features and simple implementation code first:
public SampleService(IDateTimeProvider dateTimeProvider)
{
this.dateTimeProvider = dateTimeProvider ?? throw new ArgumentNullException(nameof(dateTimeProvider));
}When a null guard policy test adds value, keep it explicit and close to the type being tested.
namespace SampleAppUnitTests;
using Library.UnitTesting.Common;
using Library.UnitTesting.Policy;
using Moq;
using NUnit.Framework;
using SampleApp;
using SampleApp.Contracts;
[TestFixture]
public class SampleServicePolicyTests : UnitTestBase<SampleService>
{
private readonly Mock<IDateTimeProvider> dateTimeProviderMock = new();
[Test]
public void Constructor_NullDependencies_ShouldThrowArgumentNullException()
{
typeof(SampleService).ShouldGuardConstructorNulls();
}
[Test]
public async Task PublicMethods_NullArguments_ShouldThrowArgumentNullException()
{
var target = this.GetTarget();
await target.ShouldGuardMethodNullsAsync();
}
protected override void Setup()
{
this.dateTimeProviderMock.Reset();
}
protected override SampleService GetTarget()
{
return new SampleService(this.dateTimeProviderMock.Object);
}
}NullGuardAssertionOptions can be used when a type needs explicit exclusions or custom parameter creation.
var options = new NullGuardAssertionOptions();
options.ExcludeConstructorParameter("optionalDependency");
options.ExcludeMethod("LegacyMethod");
options.ExcludeMethodParameter("Search", "optionalFilter");
options.TypeCreationOverrides[typeof(CustomerId)] = () => new CustomerId(42);
typeof(MyService).ShouldGuardConstructorNulls(options);async void should almost never appear in production code.
It is difficult to await, difficult to observe, and very easy to break silently.
Library.UnitTesting provides explicit checks for this policy:
[Test]
public void Type_ShouldNotContainAsyncVoidMethods()
{
typeof(SampleService).ShouldNotContainAsyncVoidMethods();
}You can also check an entire assembly:
[Test]
public void Assembly_ShouldNotContainAsyncVoidMethods()
{
typeof(SampleService).Assembly.ShouldNotContainAsyncVoidMethods();
}Event-handler-shaped async void methods are ignored by this policy check.
A good Library.UnitTesting test usually follows this shape:
[Test]
public void MethodName_StateUnderTest_ExpectedBehavior()
{
// Arrange
var input = this.Instantiator.Random<SomeInput>();
this.someDependencyMock
.Setup(x => x.GetValue(input.Id))
.Returns("value");
var target = this.GetTarget();
// Act
var actual = target.MethodName(input);
// Assert
actual.ShouldBe("value");
this.someDependencyMock
.Verify(x => x.GetValue(input.Id), Times.Once);
}The preferred style is:
- One behavior per test
- Arrange only what matters
- Prefer readable assertions
- Prefer explicit mocks over hidden magic
- Use generated data for irrelevant details
- Avoid policy tests unless they are truly useful
A simple solution can be structured like this:
Source/
Library.UnitTesting/
SampleApp/
Tests/
Unit/
SampleAppUnitTests/
The sample app demonstrates usage. The unit test project demonstrates the intended testing style.
CI should normally discover only real test projects, for example projects ending in *UnitTests, so sample applications do not accidentally become part of the test run.
A minimal GitHub Actions workflow might look like this:
name: CI
on:
push:
pull_request:
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --configuration Release --no-build --filter "FullyQualifiedName~UnitTests"Adjust the filter to match your solution conventions.
Library.UnitTesting is intentionally small and opinionated.
It should help teams write tests that are:
- Fast
- Clear
- Consistent
- Easy to maintain
- Low on ceremony
- High on intent
It should not become a framework that hides what the test is doing.
When a helper makes the test easier to read, use it. When it makes the test harder to understand, remove the helper and write the test plainly.
Version 2.0.0 changes the preferred assertion library from FluentAssertions to Shouldly and moves automatic policy checks out of UnitTestBase<TTarget> into explicit policy assertion methods.
When upgrading from 1.x, check for these common changes:
- Replace FluentAssertions-style assertions in tests with Shouldly where desired
- Replace automatic
UnitTestBase<TTarget>policy checks with explicit policy tests - Use
ShouldGuardConstructorNullsfor constructor null guard checks - Use
ShouldGuardMethodNullsAsyncfor method null guard checks - Use
ShouldNotContainAsyncVoidMethodsfor async-void policy checks - Review usages of removed FluentAssertions-specific
ItExtoverloads
Keep changes focused.
Good contributions usually make tests easier to read, reduce repeated setup, or clarify project conventions.
Avoid adding helpers that hide too much behavior. A test helper should feel like a labeled drawer, not a locked cabinet.
This project is licensed under the MIT License.