Noundry.Assertive

Fluent assertion library for expressive, readable unit tests and runtime validation with zero dependencies. Chain assertions naturally with a simple, discoverable API.

.NET 8.0+ Zero Dependencies MIT License
# Install via NuGet
$ dotnet add package Noundry.Assertive

Quick Start

Assertive provides a single extension method .Assert() that works on any value. From there, chain assertions fluently to validate your expectations.

using Noundry.Assertive;

// Simple assertion
result.Assert().IsNotNull().IsEqualTo(42);

// Chained with context
user.Assert()
 .WithContext("User validation")
 .IsNotNull()
 .IsOfType<User>()
 .Satisfies(u => u.IsActive, "User must be active");

Architecture

The library has a minimal, focused design built around three types:

  • AssertiveExtensions Static class providing the Assert<T>(this T value) extension method as the entry point.
  • Assertive<T> The core class holding the value and all assertion methods. Every method returns Assertive<T> enabling fluent chaining.
  • AssertionException Exception thrown on assertion failure, with Expected and Actual properties for diagnostics.

Important: The assertion chain stops at the first failure. Subsequent assertions in the chain are not evaluated after a failure.

Null Assertions

Validate whether values are null or non-null. These are typically the first assertion in any chain.

IsNotNull()

Throws AssertionException if value is null. Message: "Expected value to not be null."

// PASS
"hello".Assert().IsNotNull();
42.Assert().IsNotNull();
new object().Assert().IsNotNull();

// FAIL - throws AssertionException
string? s = null;
s.Assert().IsNotNull();

IsNull()

Throws if value is NOT null. Message: "Expected value to be null, but was '{value}'."

// PASS
string? s = null;
s.Assert().IsNull();

// FAIL
"hello".Assert().IsNull();

Equality Assertions

Compare values using EqualityComparer<T>.Default.Equals() for type-safe comparison.

IsEqualTo(T expected)

Message: "Expected value to be '{expected}', but was '{actual}'." The exception includes Expected and Actual properties.

42.Assert().IsEqualTo(42); // PASS
"hello".Assert().IsEqualTo("hello"); // PASS
true.Assert().IsEqualTo(true); // PASS

42.Assert().IsEqualTo(43); // FAIL - Expected: 43, Actual: 42

IsNotEqualTo(T unexpected)

Message: "Expected value to not be '{unexpected}', but it was."

42.Assert().IsNotEqualTo(100); // PASS
"hello".Assert().IsNotEqualTo("world"); // PASS

42.Assert().IsNotEqualTo(42); // FAIL

Type Assertions

Check runtime types using the is operator, which supports inheritance and interfaces.

// Exact type
"hello".Assert().IsOfType<string>();

// Inheritance and interfaces
new List<int>().Assert().IsOfType<IEnumerable<int>>(); // PASS
new List<int>().Assert().IsOfType<object>(); // PASS

// Negative check
"hello".Assert().IsNotOfType<int>(); // PASS

// Null is never of any type
string? s = null;
s.Assert().IsOfType<string>(); // FAIL - "Expected type 'String', but found 'null'."

Predicate Assertions

Use custom lambda expressions for flexible validation logic. The optional message parameter provides descriptive failure messages.

Satisfies(Func<T, bool> predicate, string? message)

Throws if predicate returns false. Default message: "Value did not satisfy the specified condition."

10.Assert().Satisfies(x => x > 5);
10.Assert().Satisfies(x => x > 5, "Value must be greater than 5");
"hello".Assert().Satisfies(s => s.Length > 3, "String too short");

// Complex predicates
var list = new List<int> { 1, 2, 3 };
list.Assert().Satisfies(
 l => l.Count == 3 && l.All(x => x > 0),
 "Invalid list state");

Fails(Func<T, bool> predicate, string? message)

Inverse of Satisfies -- throws if predicate returns true. Default message: "Value unexpectedly satisfied the condition."

10.Assert().Fails(x => x < 0, "Value should not be negative");
"hello".Assert().Fails(
 s => string.IsNullOrEmpty(s),
 "String should not be empty");

Range Assertions

Validate that comparable values fall within inclusive bounds. Requires the type to implement IComparable<T>.

IsInRange(T min, T max)

Both boundaries are inclusive. Works with int, double, decimal, char, DateTime, and any IComparable<T> type.

// Integer range
5.Assert().IsInRange(1, 10); // PASS
1.Assert().IsInRange(1, 10); // PASS - inclusive boundary
10.Assert().IsInRange(1, 10); // PASS - inclusive boundary

// Other comparable types
5.5.Assert().IsInRange(1.0, 10.0); // double
5.5m.Assert().IsInRange(1.0m, 10.0m); // decimal
'M'.Assert().IsInRange('A', 'Z'); // char
DateTime.Now.Assert().IsInRange(
 DateTime.Today,
 DateTime.Today.AddDays(1)); // DateTime

// FAIL
0.Assert().IsInRange(1, 10); // Below range
11.Assert().IsInRange(1, 10); // Above range

Collection Assertions

Validate collections including lists, arrays, and hash sets. Collection methods require specifying the element type as a generic parameter.

Important: You must specify the element type parameter, e.g. .IsEmpty<int>() not .IsEmpty().

Method Description
Contains<TItem>(item) Item must exist in the collection
DoesNotContain<TItem>(item) Item must not exist in the collection
IsEmpty<TItem>() Collection must have zero elements
IsNotEmpty<TItem>() Collection must have at least one element
HasCount<TItem>(count) Collection must have exactly count elements
var list = new List<int> { 1, 2, 3 };

list.Assert().Contains(2); // PASS
list.Assert().DoesNotContain(99); // PASS
list.Assert().IsNotEmpty<int>(); // PASS
list.Assert().HasCount<int>(3); // PASS

new List<int>().Assert().IsEmpty<int>(); // PASS

var array = new[] { "a", "b", "c" };
array.Assert().Contains("b"); // PASS
array.Assert().HasCount<string>(3); // PASS

Context and Value Access

WithContext(string context)

Adds a descriptive context prefix to all subsequent error messages in the chain. The format becomes [context] original message.

// Without context
42.Assert().IsEqualTo(18);
// Message: "Expected value to be '18', but was '42'."

// With context
42.Assert()
 .WithContext("User age validation")
 .IsEqualTo(18);
// Message: "[User age validation] Expected value to be '18', but was '42'."

// Context applies to all subsequent assertions in the chain
user.Assert()
 .WithContext("User validation")
 .IsNotNull()
 .IsOfType<User>()
 .Satisfies(u => u.IsActive, "User must be active");

Value Property

Access the underlying value after assertions pass. Useful for extracting validated values.

var result = someValue.Assert()
 .IsNotNull()
 .IsOfType<string>();
string validatedValue = result.Value;

Exception Handling

AssertionException extends Exception and provides structured failure information.

Property Type Description
Message string Human-readable error description
Expected object? What was expected (may be null)
Actual object? What was actually found (may be null)
try
{
 value.Assert()
 .WithContext("Validation context")
 .IsNotNull()
 .IsEqualTo(expected);
}
catch (AssertionException ex)
{
 Console.WriteLine($"Assertion failed: {ex.Message}");
 Console.WriteLine($"Expected: {ex.Expected}");
 Console.WriteLine($"Actual: {ex.Actual}");
}

Real-World Patterns

Test Framework Integration

Works with NUnit, xUnit, MSTest, and any other test framework.

// xUnit
[Fact]
public void Should_Calculate_Correct_Total()
{
 var total = calculator.Sum(1, 2, 3);

 total.Assert()
 .IsEqualTo(6)
 .IsInRange(0, 100);
}

// NUnit
[Test]
public void Should_Return_Valid_User()
{
 var user = userService.GetUser(1);

 user.Assert()
 .WithContext("User retrieval")
 .IsNotNull()
 .IsOfType<User>()
 .Satisfies(u => u.Id == 1, "Wrong user ID");
}

API Response Validation

response.Assert()
 .WithContext("API Response")
 .IsNotNull()
 .Satisfies(r => r.StatusCode == 200, "Expected success status")
 .Satisfies(r => r.Data != null, "Response data missing")
 .Satisfies(r => r.Data.Items.Any(), "No items returned");

Shopping Cart Validation

items.Assert()
 .WithContext("Shopping cart")
 .IsNotNull()
 .IsNotEmpty<CartItem>()
 .HasCount<CartItem>(expectedCount)
 .Contains(specificItem);

Order Total Validation

orderTotal.Assert()
 .WithContext("Order total validation")
 .IsNotNull()
 .IsInRange(0.01m, 10000.00m)
 .Satisfies(t => t > 0, "Total must be positive");

Complete API Reference

Method Works With Requires
IsNull()Any type-
IsNotNull()Any type-
IsEqualTo(T)Any typeSame type T
IsNotEqualTo(T)Any typeSame type T
IsOfType<U>()Any type-
IsNotOfType<U>()Any type-
Satisfies()Any typeFunc<T,bool>
Fails()Any typeFunc<T,bool>
IsInRange(T,T)IComparable<T>int, double, decimal, char, DateTime, etc.
Contains<TItem>()IEnumerable<TItem>List, Array, HashSet, etc.
DoesNotContain<TItem>()IEnumerable<TItem>List, Array, HashSet, etc.
IsEmpty<TItem>()IEnumerable<TItem>List, Array, HashSet, etc.
IsNotEmpty<TItem>()IEnumerable<TItem>List, Array, HashSet, etc.
HasCount<TItem>()IEnumerable<TItem>List, Array, HashSet, etc.
WithContext()Any type-
ValueAny type-

Edge Cases

Empty string vs null

An empty string is not null. "" passes IsNotNull() but fails IsNull().

Collection type mismatch

Using the wrong element type parameter fails: intList.Assert().Contains<string>("2") fails with "Value is not an enumerable of String."

Default values

default(int).Assert().IsEqualTo(0) passes, default(bool).Assert().IsEqualTo(false) passes.

Null predicates

Passing null to Satisfies() or Fails() throws ArgumentNullException, not AssertionException.

Related Packages