Testing
Value objects generated by ZeroAlloc.ValueObjects implement standard .NET equality contracts (IEquatable<T>, Equals(object?), GetHashCode(), ==, !=). No special test helpers are required — every test framework's built-in equality assertions work directly.
Test framework support
The examples below use xUnit, but the same patterns apply to NUnit and MSTest. No adapter packages or special configuration are needed.
| Framework | Equality assertion | Inequality assertion |
|---|---|---|
| xUnit | Assert.Equal(a, b) | Assert.NotEqual(a, b) |
| NUnit | Assert.That(a, Is.EqualTo(b)) | Assert.That(a, Is.Not.EqualTo(b)) |
| MSTest | Assert.AreEqual(a, b) | Assert.AreNotEqual(a, b) |
Asserting equality
Two value object instances with identical property values are equal.
[ValueObject]
public partial class Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency) => (Amount, Currency) = (amount, currency);
}
// xUnit
[Fact]
public void Equals_ReturnsTrue_WhenPropertiesMatch()
{
var a = new Money(10m, "USD");
var b = new Money(10m, "USD");
Assert.Equal(a, b); // IEquatable<Money> path
Assert.True(a == b); // operator ==
Assert.Equal(a.GetHashCode(), b.GetHashCode()); // hash codes must match
}
Asserting inequality
[Fact]
public void Equals_ReturnsFalse_WhenPropertiesDiffer()
{
var a = new Money(10m, "USD");
var b = new Money(20m, "USD");
Assert.NotEqual(a, b);
Assert.True(a != b);
}
Using value objects as dictionary keys
GetHashCode() and Equals() are consistent, so value objects can be used directly as Dictionary<TKey, TValue> keys or in HashSet<T> collections.
[Fact]
public void CanBeUsedAsDictionaryKey()
{
var dict = new Dictionary<Money, string>();
var key = new Money(10m, "USD");
dict[key] = "ten dollars";
// A new instance with the same values retrieves the same entry
Assert.Equal("ten dollars", dict[new Money(10m, "USD")]);
}
[Fact]
public void CanBeUsedInHashSet()
{
var set = new HashSet<Money>
{
new(10m, "USD"),
new(10m, "USD"), // duplicate — not added again
new(20m, "EUR"),
};
Assert.Equal(2, set.Count);
}
Struct value objects
Struct value objects follow the same patterns. No boxing occurs during equality comparisons.
[ValueObject]
public partial struct CustomerId
{
public int Value { get; }
public CustomerId(int value) => Value = value;
}
[Fact]
public void StructEquals_ReturnsTrue_WhenValuesMatch()
{
var a = new CustomerId(42);
var b = new CustomerId(42);
Assert.Equal(a, b);
Assert.True(a == b);
}
[Fact]
public void StructCanBeUsedInHashSet()
{
var set = new HashSet<CustomerId> { new(1), new(1), new(2) };
Assert.Equal(2, set.Count);
}
Testing opt-in and opt-out member selection
When [EqualityMember] is used (opt-in mode), only marked properties participate in equality. Unmarked properties are ignored.
[ValueObject]
public partial class Address
{
[EqualityMember] public string Street { get; }
[EqualityMember] public string City { get; }
public string Notes { get; } // ignored — not marked
public Address(string street, string city, string notes)
=> (Street, City, Notes) = (street, city, notes);
}
[Fact]
public void EqualityIgnoresUnmarkedProperties()
{
var a = new Address("1 Main St", "Springfield", "notes A");
var b = new Address("1 Main St", "Springfield", "notes B");
// Notes differs, but it is not an equality member
Assert.Equal(a, b);
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
When [IgnoreEqualityMember] is used (opt-out mode), all other public properties participate, and the marked property is excluded.
[ValueObject]
public partial class Product
{
public string Name { get; }
[IgnoreEqualityMember] public string InternalCode { get; }
public Product(string name, string internalCode)
=> (Name, InternalCode) = (name, internalCode);
}
[Fact]
public void EqualityIgnoresExcludedProperty()
{
var a = new Product("Widget", "CODE-001");
var b = new Product("Widget", "CODE-999");
Assert.Equal(a, b);
}
Testing nullable value objects
Properties with nullable reference types (string?, MyClass?) receive null-safe comparison in the generated Equals. Test null-vs-null (equal) and null-vs-value (not equal) explicitly.
[ValueObject]
public partial class Contact
{
public string Name { get; }
public string? Email { get; }
public Contact(string name, string? email) => (Name, Email) = (name, email);
}
[Fact]
public void NullProperties_AreEqualToNull()
{
var a = new Contact("Alice", null);
var b = new Contact("Alice", null);
Assert.Equal(a, b);
}
[Fact]
public void NullVsNonNull_IsNotEqual()
{
var a = new Contact("Alice", null);
Assert.NotEqual(a, b);
}
Common assertion patterns
Null object check
The generated Equals(object?) returns false for null without throwing.
[Fact]
public void ObjectEquals_ReturnsFalse_ForNull()
{
var money = new Money(10m, "USD");
Assert.False(money.Equals((object?)null));
}
Wrong type check
The generated Equals(object?) performs a direct type check and returns false for unrelated types.
[Fact]
public void ObjectEquals_ReturnsFalse_ForDifferentType()
{
var money = new Money(10m, "USD");
Assert.False(money.Equals("10 USD"));
}
ToString format
The generated ToString() produces "TypeName { Prop1 = Value1, Prop2 = Value2 }".
[Fact]
public void ToString_ContainsPropertyValues()
{
var money = new Money(10m, "USD");
Assert.Contains("10", money.ToString(), StringComparison.Ordinal);
Assert.Contains("USD", money.ToString(), StringComparison.Ordinal);
}