Fluent assertion library for expressive, readable unit tests and runtime validation with zero dependencies. Chain assertions naturally with a simple, discoverable API.
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");
The library has a minimal, focused design built around three types:
Assert<T>(this T value) extension method as the entry point.
Assertive<T> enabling fluent chaining.
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.
Validate whether values are null or non-null. These are typically the first assertion in any chain.
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();
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();
Compare values using EqualityComparer<T>.Default.Equals() for type-safe comparison.
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
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
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'."
Use custom lambda expressions for flexible validation logic. The optional message parameter provides descriptive failure messages.
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");
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");
Validate that comparable values fall within inclusive bounds. Requires the type to implement IComparable<T>.
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
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
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");
Access the underlying value after assertions pass. Useful for extracting validated values.
var result = someValue.Assert()
.IsNotNull()
.IsOfType<string>();
string validatedValue = result.Value;
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}");
}
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");
}
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");
items.Assert()
.WithContext("Shopping cart")
.IsNotNull()
.IsNotEmpty<CartItem>()
.HasCount<CartItem>(expectedCount)
.Contains(specificItem);
orderTotal.Assert()
.WithContext("Order total validation")
.IsNotNull()
.IsInRange(0.01m, 10000.00m)
.Satisfies(t => t > 0, "Total must be positive");
| Method | Works With | Requires |
|---|---|---|
| IsNull() | Any type | - |
| IsNotNull() | Any type | - |
| IsEqualTo(T) | Any type | Same type T |
| IsNotEqualTo(T) | Any type | Same type T |
| IsOfType<U>() | Any type | - |
| IsNotOfType<U>() | Any type | - |
| Satisfies() | Any type | Func<T,bool> |
| Fails() | Any type | Func<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 | - |
| Value | Any type | - |
An empty string is not null. "" passes IsNotNull() but fails IsNull().
Using the wrong element type parameter fails: intList.Assert().Contains<string>("2") fails with "Value is not an enumerable of String."
default(int).Assert().IsEqualTo(0) passes, default(bool).Assert().IsEqualTo(false) passes.
Passing null to Satisfies() or Fails() throws ArgumentNullException, not AssertionException.