Skip to main content

Testing

Overview

The ZeroAlloc.Validation.Testing NuGet package provides ValidationAssert — a set of static helpers for writing clean, readable validator tests.

Install:

dotnet add package ZeroAlloc.Validation.Testing

ValidationAssert methods

// Passes if result has any failure for propertyName; throws ValidationAssertException otherwise
ValidationAssert.HasError(result, "Email");

// Passes if result has a failure for propertyName with exactly that message; throws otherwise
ValidationAssert.HasErrorWithMessage(result, "Email", "Email must be a valid email address.");

// Passes if result.IsValid is true (zero failures); throws otherwise
ValidationAssert.NoErrors(result);

ValidationAssertException (extends Exception) is thrown on assertion failure with a descriptive message. Because the exception type is framework-agnostic, ValidationAssert works with any test framework — xUnit, NUnit, MSTest, or otherwise.

Example test — flat model

using ZeroAlloc.Validation.Testing;
using Xunit;

public class CreateOrderRequestValidatorTests
{
private readonly CreateOrderRequestValidator _validator = new();

[Fact]
public void Valid_order_passes()
{
var request = new CreateOrderRequest
{
Reference = "ORD-001",
Amount = 100m,
};

var result = _validator.Validate(request);
ValidationAssert.NoErrors(result);
}

[Fact]
public void Empty_email_fails()
{
var request = new CreateOrderRequest { Reference = "ORD-001", Amount = 100m, Email = "" };
var result = _validator.Validate(request);

ValidationAssert.HasError(result, "Email");
}

[Fact]
public void Invalid_email_format_fails_with_message()
{
var request = new CreateOrderRequest { Reference = "ORD-001", Amount = 100m, Email = "not-an-email" };
var result = _validator.Validate(request);

ValidationAssert.HasErrorWithMessage(result, "Email", "Email must be a valid email address.");
}
}

Example test — nested model

Nested validator instances are passed manually via constructor injection:

public class OrderValidatorTests
{
private readonly OrderValidator _validator = new(new AddressValidator());

[Fact]
public void Null_address_fails()
{
var order = new Order { Reference = "ORD-001", ShippingAddress = null };
var result = _validator.Validate(order);

ValidationAssert.HasError(result, "ShippingAddress");
}

[Fact]
public void Nested_property_failure_is_prefixed()
{
var order = new Order
{
Reference = "ORD-001",
ShippingAddress = new Address { Street = "", City = "London", PostalCode = "EC1A 1BB" }
};

var result = _validator.Validate(order);
ValidationAssert.HasError(result, "ShippingAddress.Street");
}
}

Failures from a nested validator are prefixed with the parent property name, so Street inside ShippingAddress becomes ShippingAddress.Street.

Example test — collection model

Collection item failures are prefixed with the collection property name and the zero-based index of the failing item:

public class CartValidatorTests
{
private readonly CartValidator _validator = new(new LineItemValidator());

[Fact]
public void Invalid_collection_item_uses_index_prefix()
{
var cart = new Cart
{
CartId = "CART-001",
Items = new List<LineItem>
{
new LineItem { Sku = "SKU-A", Quantity = 1 },
new LineItem { Sku = "", Quantity = 0 }
}
};

var result = _validator.Validate(cart);
ValidationAssert.HasError(result, "Items[1].Sku");
ValidationAssert.HasError(result, "Items[1].Quantity");
}
}

Testing without ValidationAssert

You can also assert directly on result.Failures using your preferred framework:

var result = _validator.Validate(request);

Assert.False(result.IsValid);
Assert.Contains(result.Failures.ToArray(), f => f.PropertyName == "Email");

result.Failures is ReadOnlySpan<ValidationFailure>. Call .ToArray() or iterate with foreach (ref readonly var f in result.Failures) if your assertion library does not support spans directly.