A pytest inspired assertion library for .NET with no special syntax.
SharpAssert provides rich assertion diagnostics by automatically transforming your assertion expressions at compile time using MSBuild source rewriting, giving you detailed failure messages with powerful expression analysis.
using static SharpAssert.Sharp;
var items = new[] { 1, 2, 3 };
var target = 4;
Assert(items.Contains(target));
// Assertion failed: items.Contains(target) at MyTest.cs:15
// items: [1, 2, 3]
// target: 4
// Result: falseSharpAssert uses MSBuild source rewriting to automatically transform your assertion calls at compile time:
- You write:
Assert(x == y) - MSBuild rewrites:
global::SharpAssert.SharpInternal.Assert(() => x == y, "x == y", "file.cs", 42) - Runtime analysis: Expression tree provides detailed failure diagnostics when assertions fail
- 🔍 Detailed Expression Analysis - See exactly why your assertions failed
- 🎯 Exception Testing -
Throws<T>andThrowsAsync<T>with detailed exception diagnostics - 🔤 String Diffs - Character-level inline diffs for strings (powered by DiffPlex)
- 📊 Collection Comparison - First mismatch, missing/extra elements detection
- 🔎 Object Deep Diff - Property-level differences for objects/records (powered by Compare-Net-Objects)
- 🔗 LINQ Operations - Enhanced diagnostics for Contains/Any/All operations
- ⚡ Async/Await Support - Full support for async assertions with value diagnostics
- 💫 Dynamic Types - Dynamic objects support (Expando)
See demo for assertion example output.
dotnet add package SharpAssertusing static SharpAssert.Sharp;
[Test]
public void Should_be_equal()
{
var expected = 4;
var actual = 5;
Assert(expected == actual);
// Assertion failed: expected == actual
// Left: 4
// Right: 5
// Result: false
}Character-level diffs powered by DiffPlex:
var actual = "hello";
var expected = "hallo";
Assert(actual == expected);
// Assertion failed: actual == expected
// String diff (inline):
// h[-e][+a]lloMultiline string diffs:
var actual = "line1\nline2\nline3";
var expected = "line1\nMODIFIED\nline3";
Assert(actual == expected);
// Assertion failed: actual == expected
// String diff:
// line1
// - line2
// + MODIFIED
// line3First mismatch and missing/extra elements:
var actual = new[] { 1, 2, 3, 5 };
var expected = new[] { 1, 2, 4, 5 };
Assert(actual.SequenceEqual(expected));
// Assertion failed: actual.SequenceEqual(expected)
// Collections differ at index 2:
// Expected: 4
// Actual: 3Property-level diffs powered by Compare-Net-Objects:
var actual = new User { Name = "John", Age = 30, City = "NYC" };
var expected = new User { Name = "John", Age = 25, City = "LA" };
Assert(actual == expected);
// Assertion failed: actual == expected
// Object differences:
// Age: 30 → 25
// City: "NYC" → "LA"Enhanced diagnostics for Contains, Any, All:
var users = new[] { "Alice", "Bob", "Charlie" };
Assert(users.Contains("David"));
// Assertion failed: users.Contains("David")
// Collection: ["Alice", "Bob", "Charlie"]
// Looking for: "David"
// Result: falseFull support for async expressions:
Assert(await client.GetAsync() == await server.GetAsync());
// Assertion failed: await client.GetAsync() == await server.GetAsync()
// Left: { Id: 1, Name: "Client" }
// Right: { Id: 2, Name: "Server" }
// Result: falseTest expected exceptions with Throws<T> and ThrowsAsync<T>:
// Positive assertion - expects exception
Assert(Throws<ArgumentException>(() => throw new ArgumentException("invalid")));
// Negative assertion - expects no exception
Assert(!Throws<ArgumentException>(() => { /* no exception */ }));
// Access exception properties
var ex = Throws<ArgumentNullException>(() => throw new ArgumentNullException("param"));
Assert(ex.Message.Contains("param"));
// Async version
Assert(await ThrowsAsync<InvalidOperationException>(() =>
Task.Run(() => throw new InvalidOperationException())));Assert(user.IsActive, $"User {user.Name} should be active for this operation");var order = new Order { Items = new[] { "Coffee", "Tea" }, Total = 15.50m };
var expectedTotal = 12.00m;
Assert(order.Items.Length > 0 && order.Total == expectedTotal);
// Assertion failed: order.Items.Length > 0 && order.Total == expectedTotal
// order.Items.Length > 0 → True (Length: 2)
// order.Total == expectedTotal → False
// order.Total: 15.50
// expectedTotal: 12.00
// Result: FalseSharpAssert is built on modern .NET technologies:
- MSBuild Source Rewriting - Compile-time code transformation
- Roslyn Syntax Analysis - Advanced C# code parsing and generation
- Expression Trees - Runtime expression analysis for rich diagnostics
- DiffPlex - String and sequence diffs
- CompareNETObjects - Deep object comparison
- CallerArgumentExpression - Fallback for edge cases
- .NET 9.0 or later
- Test frameworks: xUnit, NUnit, or MSTest
SharpAssert consists of two NuGet packages:
- SharpAssert - Main package with MSBuild rewriter (install this one)
- SharpAssert.Runtime - Core assertion library (automatically included as dependency)
When you install SharpAssert, you get everything you need. The runtime package is a transitive dependency and requires no separate installation.
Enable detailed rewriter diagnostics:
<PropertyGroup>
<!-- Enable diagnostic logging for troubleshooting rewriter issues -->
<SharpAssertEmitRewriteInfo>true</SharpAssertEmitRewriteInfo>
</PropertyGroup>SharpAssert is designed for minimal overhead:
- Passing tests: Near-zero overhead - only the assertion check itself
- Failing tests: Rich diagnostics are computed only when assertions fail
- Expression evaluation: Each sub-expression evaluated exactly once (cached)
- Build time: Negligible impact - rewriter processes only test files
The rich diagnostic tools (object diffing, collection comparison) are only invoked on failure. This means your passing tests run at full speed, and the diagnostic cost is only paid when you need to understand a failure.
- Collection initializers cannot be used in expression trees (C# compiler limitation)
- Use
new[]{1,2,3}instead of[1, 2, 3]
- Use
- Verify
SharpAssertpackage is installed (SharpAssert.Runtime comes automatically) - Ensure
using static SharpAssert.Sharp;import - Clean and rebuild:
dotnet clean && dotnet build
- Check build output contains: "SharpAssert: Rewriting X source files"
- Verify rewritten files exist in
obj/Debug/net9.0/SharpRewritten/ - Ensure
SharpInternal.Assertcalls are being made (check generated code) - Look for #line directives in generated files
For troubleshooting rewriter issues:
<PropertyGroup>
<SharpAssertEmitRewriteInfo>true</SharpAssertEmitRewriteInfo>
</PropertyGroup>Then rebuild with verbose output: dotnet build -v detailed
We welcome contributions! Please see our comprehensive Contributing Guide for:
- 🚀 Quick start guide for developers
- 🧪 Testing strategy and workflow
- 📦 Package versioning best practices
- 🔧 Development tips and debugging help
- 📝 Commit guidelines and release process
