Skip to main content

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.

FrameworkEquality assertionInequality assertion
xUnitAssert.Equal(a, b)Assert.NotEqual(a, b)
NUnitAssert.That(a, Is.EqualTo(b))Assert.That(a, Is.Not.EqualTo(b))
MSTestAssert.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);
var b = new Contact("Alice", "[email protected]");
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);
}