C0deGeek.DeepAssert
1.0.0
dotnet add package C0deGeek.DeepAssert --version 1.0.0
NuGet\Install-Package C0deGeek.DeepAssert -Version 1.0.0
<PackageReference Include="C0deGeek.DeepAssert" Version="1.0.0" />
<PackageVersion Include="C0deGeek.DeepAssert" Version="1.0.0" />
<PackageReference Include="C0deGeek.DeepAssert" />
paket add C0deGeek.DeepAssert --version 1.0.0
#r "nuget: C0deGeek.DeepAssert, 1.0.0"
#:package C0deGeek.DeepAssert@1.0.0
#addin nuget:?package=C0deGeek.DeepAssert&version=1.0.0
#tool nuget:?package=C0deGeek.DeepAssert&version=1.0.0
C0deGeek.DeepAssert
A lightweight, powerful assertion library for .NET that provides deep object comparison capabilities for unit testing. DeepAssert enables complex object equivalency checks with fine-grained control over comparison behavior, making it perfect for effective test-driven development.
Table of Contents
- Features
- Installation
- Quick Start
- Core Functionality
- Advanced Usage
- Integration
- Best Practices
- Examples
- Performance Considerations
- Contributing
- License
Features
- Deep object equality checking with proper handling of complex object graphs
- Circular reference detection to prevent stack overflows
- Fine-grained control over what properties to include/exclude in comparison
- Collection comparison with ordered and unordered options
- AssertionScope to collect and report multiple assertion failures at once
- Exception testing utilities for verifying exception types and messages
- Type conversion handling for numeric types and other common conversions
- Path tracking to easily identify differences in complex objects
- Simple integration with major testing frameworks (MSTest, xUnit, NUnit)
- Zero dependencies outside of standard .NET libraries
Installation
dotnet add package C0deGeek.DeepAssert
Quick Start
// Import the namespace
using C0deGeek.DeepAssert;
// Basic comparison
AssertExtensions.Equivalent(expected, actual);
// With exclusions
AssertExtensions.Equivalent(expected, actual, opt =>
opt.Excluding<User>(u => u.LastModified));
// Collection comparison
AssertExtensions.EquivalentCollection(expectedList, actualList);
// Exception testing
Exception ex = AssertExtensions.ThrowsExactly<ArgumentException>(() =>
DoSomethingThatThrows());
// Multiple assertions with AssertionScope
using (new AssertionScope())
{
AssertExtensions.Equivalent(user.Id, expectedId);
AssertExtensions.Equivalent(user.Name, expectedName);
AssertExtensions.Equivalent(user.Email, expectedEmail);
}
Core Functionality
Basic Assertions
// Simple value comparison
AssertExtensions.Equivalent(1, 1);
AssertExtensions.Equivalent("test", "test");
AssertExtensions.Equivalent(3.14, 3.14);
// Reference type comparison
var user1 = new User { Id = 1, Name = "Test" };
var user2 = new User { Id = 1, Name = "Test" };
AssertExtensions.Equivalent(user1, user2);
// Null handling
AssertExtensions.Equivalent(null, null);
Deep Object Equivalency
// Complex object comparison
var order1 = new Order
{
Id = 1,
Customer = new Customer { Id = 100, Name = "John" },
Items = new List<OrderItem>
{
new() { ProductId = 1, Quantity = 2, Price = 10.0m },
new() { ProductId = 2, Quantity = 1, Price = 15.0m }
}
};
var order2 = new Order
{
Id = 1,
Customer = new Customer { Id = 100, Name = "John" },
Items = new List<OrderItem>
{
new() { ProductId = 1, Quantity = 2, Price = 10.0m },
new() { ProductId = 2, Quantity = 1, Price = 15.0m }
}
};
AssertExtensions.Equivalent(order1, order2);
Collection Assertions
// Ordered collection comparison
var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 1, 2, 3 };
AssertExtensions.EquivalentCollection(list1, list2, opt => opt.AllowUnordered = false);
// Unordered collection comparison
var list3 = new List<int> { 3, 1, 2 };
AssertExtensions.EquivalentCollection(list1, list3); // AllowUnordered = true by default
// Collection of complex objects
var users1 = new List<User> { new() { Id = 1, Name = "Alice" }, new() { Id = 2, Name = "Bob" } };
var users2 = new List<User> { new() { Id = 1, Name = "Alice" }, new() { Id = 2, Name = "Bob" } };
AssertExtensions.EquivalentCollection(users1, users2);
Exclusion Options
// Exclude single property
AssertExtensions.Equivalent(user1, user2, opt =>
opt.Excluding<User>(u => u.LastModified));
// Exclude multiple properties
AssertExtensions.Equivalent(user1, user2, opt =>
opt.Excluding<User>(u => u.LastModified)
.Excluding<User>(u => u.CreatedBy));
// Exclude nested property
AssertExtensions.Equivalent(order1, order2, opt =>
opt.Excluding<Order>(o => o.Customer!.Address!));
// Exclude properties for collection elements
AssertExtensions.EquivalentCollection(orders1, orders2, opt =>
opt.Excluding<Order>(o => o.OrderDate));
Exception Testing
// Test for exact exception type
var exception = AssertExtensions.ThrowsExactly<ArgumentNullException>(() =>
SomeMethod(null));
// Check exception properties
Assert.AreEqual("paramName", exception.ParamName);
// Async exception testing
var asyncException = await AssertExtensions.ThrowsExactlyAsync<TimeoutException>(() =>
SomeAsyncMethod());
AssertionScope
// Multiple assertions collected together
using (new AssertionScope())
{
AssertExtensions.Equivalent(user.Id, expectedId);
AssertExtensions.Equivalent(user.Name, expectedName);
AssertExtensions.Equivalent(user.Email, expectedEmail);
AssertExtensions.Equivalent(user.IsActive, true);
}
// If any fail, all failures will be reported together
// Can be used with standard Assert methods too
using (new AssertionScope())
{
Assert.AreEqual(expected.Id, actual.Id);
Assert.IsTrue(actual.IsValid);
AssertExtensions.EquivalentCollection(expected.Items, actual.Items);
}
// Nested scopes
using (new AssertionScope()) // Outer scope
{
AssertExtensions.Equivalent(user.Id, expectedId);
// Inner scope for address
using (new AssertionScope())
{
AssertExtensions.Equivalent(user.Address.Street, expectedAddress.Street);
AssertExtensions.Equivalent(user.Address.City, expectedAddress.City);
}
}
Advanced Usage
Handling Circular References
// Objects with circular references
public class Node
{
public string Value { get; set; }
public Node Parent { get; set; }
public List<Node> Children { get; set; } = new();
}
var root1 = new Node { Value = "Root" };
var child1 = new Node { Value = "Child", Parent = root1 };
root1.Children.Add(child1);
var root2 = new Node { Value = "Root" };
var child2 = new Node { Value = "Child", Parent = root2 };
root2.Children.Add(child2);
// Automatic circular reference detection
AssertExtensions.Equivalent(root1, root2);
Numeric Comparison
// Comparing different numeric types
AssertExtensions.Equivalent(5, 5.0); // int vs double
AssertExtensions.Equivalent(5.0m, 5); // decimal vs int
AssertExtensions.Equivalent(5, 5L); // int vs long
// Floating point comparison
var a = 0.1 + 0.2; // 0.30000000000000004 in floating point
var b = 0.3;
// These values aren't exactly equal due to floating point precision
// But DeepAssert handles this intelligently
AssertExtensions.Equivalent(a, b);
Customizing Type Comparison
// Create a custom equivalency options for specific test needs
var options = new EquivalencyOptions();
options.Excluding("LastModified");
options.Excluding("CreatedBy");
options.AllowUnordered = true;
// Use in multiple assertions
AssertExtensions.Equivalent(user1, user2, opt => options);
AssertExtensions.EquivalentCollection(userList1, userList2, opt => options);
Path Tracking
// When an assertion fails, the detailed error message includes the property path
var order1 = new Order
{
Id = 1,
Customer = new Customer { Id = 100, Name = "John" },
Items = new List<OrderItem>
{
new() { ProductId = 1, Quantity = 2, Price = 10.0m }
}
};
var order2 = new Order
{
Id = 1,
Customer = new Customer { Id = 100, Name = "John" },
Items = new List<OrderItem>
{
new() { ProductId = 1, Quantity = 3, Price = 10.0m } // Different quantity
}
};
// This will fail with a message like:
// Items[0].Quantity: expected <2>, actual <3>
AssertExtensions.Equivalent(order1, order2);
Integration
MSTest Integration
// Works out of the box with MSTest
using Microsoft.VisualStudio.TestTools.UnitTesting;
using C0deGeek.DeepAssert;
[TestClass]
public class UserTests
{
[TestMethod]
public void User_Should_BeEquivalent()
{
var user1 = new User { Id = 1, Name = "Test" };
var user2 = new User { Id = 1, Name = "Test" };
AssertExtensions.Equivalent(user1, user2);
}
[TestMethod]
public void Users_WithDifferentCasing_ShouldBeEquivalent()
{
var user1 = new User { Name = "test" };
var user2 = new User { Name = "TEST" };
// Normal assertion would fail
// But we can configure comparison
AssertExtensions.Equivalent(user1, user2, opt =>
// Custom configuration would go here
);
}
}
NUnit Integration
// Works with NUnit as well
using NUnit.Framework;
using C0deGeek.DeepAssert;
[TestFixture]
public class OrderTests
{
[Test]
public void Orders_Should_BeEquivalent()
{
var order1 = CreateTestOrder();
var order2 = CreateTestOrder();
AssertExtensions.Equivalent(order1, order2);
}
[Test]
public void Orders_ShouldBeEquivalent_ExcludingTimestamps()
{
var order1 = CreateTestOrder();
var order2 = CreateTestOrder();
order2.CreatedAt = DateTime.Now.AddDays(1); // Different timestamp
AssertExtensions.Equivalent(order1, order2, opt =>
opt.Excluding<Order>(o => o.CreatedAt));
}
}
xUnit Integration
// Works with xUnit as well
using Xunit;
using C0deGeek.DeepAssert;
public class ProductTests
{
[Fact]
public void Products_Should_BeEquivalent()
{
var product1 = new Product { Id = 1, Name = "Test", Price = 9.99m };
var product2 = new Product { Id = 1, Name = "Test", Price = 9.99m };
AssertExtensions.Equivalent(product1, product2);
}
[Fact]
public void MultipleAssertions_Should_BeReported()
{
using (new AssertionScope())
{
AssertExtensions.Equivalent(1, 1);
AssertExtensions.Equivalent("test", "test");
AssertExtensions.Equivalent(true, true);
}
}
}
Best Practices
Use AssertionScope for Multiple Checks
using (new AssertionScope()) { AssertExtensions.Equivalent(user.Id, expectedId); AssertExtensions.Equivalent(user.Name, expectedName); AssertExtensions.Equivalent(user.Email, expectedEmail); }
Be Specific About What to Exclude
// Good: Specifically exclude the properties you don't care about AssertExtensions.Equivalent(user1, user2, opt => opt.Excluding<User>(u => u.LastModified)); // Bad: Don't use overly broad exclusions that might hide real issues // opt.Excluding("Id"); // Could hide important differences
Handle Collections Appropriately
// Use ordered comparison when order matters AssertExtensions.EquivalentCollection(sortedList1, sortedList2, opt => opt.AllowUnordered = false); // Use unordered comparison when order doesn't matter AssertExtensions.EquivalentCollection(userSet1, userSet2);
Test for Exact Exception Types
// Good: Test for exact exception type var ex = AssertExtensions.ThrowsExactly<ArgumentNullException>(() => method(null)); // Better: Also verify exception properties Assert.AreEqual("paramName", ex.ParamName);
Structure Tests for Readability
// Arrange var expected = new Order { /* ... */ }; var actual = orderService.GetOrder(orderId); // Act & Assert using (new AssertionScope()) { // High-level properties first AssertExtensions.Equivalent(actual.Id, expected.Id); AssertExtensions.Equivalent(actual.Status, expected.Status); // Then nested objects AssertExtensions.Equivalent(actual.Customer, expected.Customer); // Then collections AssertExtensions.EquivalentCollection(actual.Items, expected.Items); }
Examples
Domain and DTO Comparison
public class UserEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastLogin { get; set; }
}
public class UserDto
{
public string Name { get; set; }
public string Email { get; set; }
}
[TestMethod]
public void UserDto_ShouldMatchEntity_ExcludingId()
{
var entity = new UserEntity
{
Id = 1,
Name = "John Doe",
Email = "john@example.com",
CreatedAt = DateTime.Now,
LastLogin = DateTime.Now.AddDays(-1)
};
var dto = new UserDto
{
Name = "John Doe",
Email = "john@example.com"
};
// Compare objects of different types, focusing only on shared properties
AssertExtensions.Equivalent(entity, dto, opt =>
opt.Excluding<UserEntity>(e => e.Id)
.Excluding<UserEntity>(e => e.CreatedAt)
.Excluding<UserEntity>(e => e.LastLogin));
}
Complex Object Graph
public class Order
{
public int Id { get; set; }
public Customer Customer { get; set; }
public List<OrderItem> Items { get; set; }
public Address ShippingAddress { get; set; }
public Address BillingAddress { get; set; }
public DateTime OrderDate { get; set; }
public OrderStatus Status { get; set; }
}
[TestMethod]
public void Order_AfterProcessing_ShouldBeInCorrectState()
{
// Arrange
var originalOrder = CreateTestOrder();
var expectedProcessedOrder = CreateExpectedProcessedOrder();
// Act
var actualProcessedOrder = orderProcessor.Process(originalOrder);
// Assert
using (new AssertionScope())
{
// ID and customer info should remain the same
AssertExtensions.Equivalent(actualProcessedOrder.Id, expectedProcessedOrder.Id);
AssertExtensions.Equivalent(actualProcessedOrder.Customer, expectedProcessedOrder.Customer);
// Status should be updated
AssertExtensions.Equivalent(actualProcessedOrder.Status, OrderStatus.Processed);
// Items should remain the same but may be reordered
AssertExtensions.EquivalentCollection(
actualProcessedOrder.Items,
expectedProcessedOrder.Items);
// Shipping/billing addresses should be unchanged
AssertExtensions.Equivalent(
actualProcessedOrder.ShippingAddress,
expectedProcessedOrder.ShippingAddress);
AssertExtensions.Equivalent(
actualProcessedOrder.BillingAddress,
expectedProcessedOrder.BillingAddress);
}
}
Testing Multiple Criteria on Collections
[TestMethod]
public void FilteredProducts_ShouldMatchCriteria()
{
// Arrange
var products = GetSampleProducts();
var criteria = new FilterCriteria
{
MinPrice = 10.0m,
MaxPrice = 50.0m,
Categories = new[] { "Electronics", "Accessories" }
};
// Act
var filteredProducts = productService.Filter(products, criteria);
// Assert
using (new AssertionScope())
{
// Check each product meets the criteria
foreach (var product in filteredProducts)
{
Assert.IsTrue(product.Price >= criteria.MinPrice,
$"Product {product.Name} price {product.Price} is below minimum {criteria.MinPrice}");
Assert.IsTrue(product.Price <= criteria.MaxPrice,
$"Product {product.Name} price {product.Price} is above maximum {criteria.MaxPrice}");
Assert.IsTrue(criteria.Categories.Contains(product.Category),
$"Product {product.Name} category {product.Category} is not in allowed categories");
}
// Check we didn't miss any products that should have been included
var shouldBeIncluded = products.Where(p =>
p.Price >= criteria.MinPrice &&
p.Price <= criteria.MaxPrice &&
criteria.Categories.Contains(p.Category)).ToList();
AssertExtensions.EquivalentCollection(filteredProducts, shouldBeIncluded);
}
}
Performance Considerations
Reuse EquivalencyOptions
Creating configuration objects repeatedly can be expensive. Consider reusing them:
// Create once private static readonly EquivalencyOptions StandardOptions = new(); static MyTests() { // Configure once StandardOptions.Excluding("CreatedAt"); StandardOptions.Excluding("LastModified"); } // Use in tests [TestMethod] public void Test1() { AssertExtensions.Equivalent(obj1, obj2, opt => StandardOptions); }
Limit Comparison Depth for Very Complex Objects
For extremely complex objects, you might want to constrain comparison depth:
// Create custom comparison options for very deep objects var options = new EquivalencyOptions(); options.MaxDepth = 5; // Limit depth to prevent performance issues AssertExtensions.Equivalent(complexObj1, complexObj2, opt => options);
Use Fast Comparisons for Collections When Possible
// For simple value types, sorted arrays can be compared efficiently Array.Sort(array1); Array.Sort(array2); // Then use EquivalentCollection with ordered comparison AssertExtensions.EquivalentCollection(array1, array2, opt => opt.AllowUnordered = false);
Contributing
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature-name
- Commit your changes:
git commit -m 'Add some feature'
- Push to the branch:
git push origin feature/your-feature-name
- Open a pull request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- MSTest.TestFramework (>= 3.6.4)
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.0.0 | 166 | 5/1/2025 |