Test your value objects
The generated code is tested in this repository. Your rules are not: a normalizer that is not idempotent, or a pattern that rejects a value the validator was written to accept, is a bug in your type. The contract kit finds those from a short list of examples.
dotnet add package AdCodicem.ValueObjects.Testing
The kit is built on xUnit v3.
Declare a contract
public sealed class IbanContract : ValueObjectContract<Iban, string>
{
protected override IEnumerable<string> AcceptedValues =>
["FR7630006000011234567890189", "DE89370400440532013000"];
protected override IEnumerable<string> RejectedValues =>
["", "not-an-iban", "FR7630006000011234567890188"];
}
Give at least two distinct accepted values, so ordering can be checked, and rejected values that exercise each rule — here an empty string, a wrong shape, and a wrong check digit.
What it checks
Each is an xUnit test in your suite:
- every accepted value produces an initialized instance;
- every rejected value is refused by
TryCreatewith a stable code and a message, andCreatethrows with that same code; - normalization settles after one pass, and creating from an already created value changes nothing;
- equality is reflexive and symmetric, and agrees with the hash code;
- ordering agrees with equality;
- text survives a round trip, and formatting into a span matches formatting into a string;
- JSON carries the bare underlying value, and rejects what the type rejects;
- the type is discoverable at run time;
- every accepted value respects the declared length limits. A type that did not register itself has no declared limits to read, and this check reports itself skipped.
The last two read the registry. They run the generated registration of the type's assembly first, so they do not depend on another test having used that assembly, which matters for contracts kept in a test project of their own.
Constructing an invalid instance on purpose
A test that needs an uninitialized instance — to check a guard, say — trips VO0010, which is a build error.
Disable it on the spot, with a comment saying why:
#pragma warning disable VO0010 // The guard under test must reject an uninitialized instance.
var missing = default(Iban);
#pragma warning restore VO0010
Identifiers in tests
New() on an [EntityId] reads an ambient clock and entropy source. ValueObjectIds.Use(clock, bytes) pins
both for the current execution flow, so a test gets the same identifier on every run —
Public identifiers shows it.