C0deGeek.DeepAssert 1.0.0

dotnet add package C0deGeek.DeepAssert --version 1.0.0
                    
NuGet\Install-Package C0deGeek.DeepAssert -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="C0deGeek.DeepAssert" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="C0deGeek.DeepAssert" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="C0deGeek.DeepAssert" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add C0deGeek.DeepAssert --version 1.0.0
                    
#r "nuget: C0deGeek.DeepAssert, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package C0deGeek.DeepAssert@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=C0deGeek.DeepAssert&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=C0deGeek.DeepAssert&version=1.0.0
                    
Install as a Cake Tool

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.

License: MIT .NET 8.0

Table of Contents

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

  1. Use AssertionScope for Multiple Checks

    using (new AssertionScope())
    {
        AssertExtensions.Equivalent(user.Id, expectedId);
        AssertExtensions.Equivalent(user.Name, expectedName);
        AssertExtensions.Equivalent(user.Email, expectedEmail);
    }
    
  2. 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
    
  3. 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);
    
  4. 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);
    
  5. 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

  1. 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);
    }
    
  2. 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);
    
  3. 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

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/your-feature-name
  3. Commit your changes: git commit -m 'Add some feature'
  4. Push to the branch: git push origin feature/your-feature-name
  5. Open a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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