Better Error Messages
TL;DR: When a response doesn't match expectations, you want to know why. Use
ValidationResultfor custom messages (built-in), or integrate with FluentAssertions for rich diff output.
The Problem: Uninformative Failures
A simple predicate with an explanation tells you that something failed, but not which part was wrong:
Expect.That<User>(
r => r.Name == expectedName && r.Email == expectedEmail,
"User fields should match expected values")
When this fails, you see:
User fields should match expected values
Not helpful. Was the name wrong? The email? Both? You have to dig through logs or add debugging to figure out what actually differed.
Option 1: ValidationResult (Built-In)
Accordant includes ValidationResult — no external dependencies required. Return ValidationResult.Invalid(message) with a clear explanation:
Expect.That<User>(response =>
{
if (response.Name != expectedName)
return ValidationResult.Invalid(
$"Name mismatch: expected '{expectedName}', got '{response.Name}'");
if (response.Email != expectedEmail)
return ValidationResult.Invalid(
$"Email mismatch: expected '{expectedEmail}', got '{response.Email}'");
if (response.Status != expectedStatus)
return ValidationResult.Invalid(
$"Status mismatch: expected '{expectedStatus}', got '{response.Status}'");
return ValidationResult.Valid();
})
When validation fails, you see exactly what went wrong:
Email mismatch: expected 'alice@example.com', got 'alice@test.com'
Checking Multiple Fields
For responses with many fields, check them all and collect errors:
Expect.That<Order>(response =>
{
var errors = new List<string>();
if (response.OrderId != expectedOrder.OrderId)
errors.Add($"OrderId: expected '{expectedOrder.OrderId}', got '{response.OrderId}'");
if (response.Status != expectedOrder.Status)
errors.Add($"Status: expected '{expectedOrder.Status}', got '{response.Status}'");
if (response.Total != expectedOrder.Total)
errors.Add($"Total: expected {expectedOrder.Total}, got {response.Total}");
if (response.Items.Count != expectedOrder.Items.Count)
errors.Add($"Items.Count: expected {expectedOrder.Items.Count}, got {response.Items.Count}");
return errors.Count == 0
? ValidationResult.Valid()
: ValidationResult.Invalid(string.Join("\n", errors));
})
Output when multiple fields differ:
Status: expected 'Shipped', got 'Pending'
Total: expected 150.00, got 145.00
Option 2: FluentAssertions
FluentAssertions is a popular library that provides rich comparison output with detailed diffs. It's particularly good for comparing complex objects.
Installation
dotnet add package FluentAssertions
Basic Integration
Wrap FluentAssertions calls in a try-catch and convert exceptions to ValidationResult:
using FluentAssertions;
Expect.That<User>(response =>
{
try
{
response.Should().BeEquivalentTo(new User
{
Id = expectedId,
Name = "Alice",
Email = "alice@example.com"
});
return ValidationResult.Valid();
}
catch (Exception ex)
{
return ValidationResult.Invalid(ex.Message);
}
})
When validation fails, FluentAssertions provides detailed output:
Expected member Name to be "Alice", but "Bob" differs near "Bob" (index 0).
Expected member Email to be "alice@example.com", but "bob@example.com" differs near "bob" (index 0).
Excluding Server-Generated Fields
Real responses often include fields you can't predict — timestamps, ETags, version numbers. Use Excluding to skip them:
Expect.That<Todo>(response =>
{
try
{
response.Should().BeEquivalentTo(new Todo
{
TodoId = expectedTodoId,
Title = expectedTitle,
Completed = false
}, options => options
.Excluding(x => x.CreatedAt) // Server-generated
.Excluding(x => x.LastModified) // Server-generated
.Excluding(x => x.ETag)); // Server-generated
return ValidationResult.Valid();
}
catch (Exception ex)
{
return ValidationResult.Invalid(ex.Message);
}
})
Comparing Collections
FluentAssertions shines when comparing lists:
Expect.That<OrderResponse>(response =>
{
try
{
response.Items.Should().BeEquivalentTo(expectedItems, options => options
.WithStrictOrdering()); // Order matters
return ValidationResult.Valid();
}
catch (Exception ex)
{
return ValidationResult.Invalid(ex.Message);
}
})
Output when collections differ:
Expected collection to contain 3 items, but found 2.
Expected item[1].Quantity to be 5, but found 3.
Helper Method
If you use FluentAssertions frequently, create a helper:
public static class ExpectHelpers
{
public static ValidationResult EquivalentTo<T>(T actual, T expected,
Func<EquivalencyAssertionOptions<T>, EquivalencyAssertionOptions<T>>? config = null)
{
try
{
if (config != null)
actual.Should().BeEquivalentTo(expected, config);
else
actual.Should().BeEquivalentTo(expected);
return ValidationResult.Valid();
}
catch (Exception ex)
{
return ValidationResult.Invalid(ex.Message);
}
}
}
// Usage becomes cleaner:
Expect.That<User>(response => ExpectHelpers.EquivalentTo(response, expectedUser,
opt => opt.Excluding(x => x.CreatedAt)))
Choosing an Approach
| Approach | Pros | Cons |
|---|---|---|
| ValidationResult | No dependencies, full control | Manual work for complex objects |
| FluentAssertions | Rich diffs, great for objects/collections | External dependency |
Recommendations:
- Simple responses (few fields) →
ValidationResultis fine - Complex objects/collections → FluentAssertions saves time
Example: Full Validation Pattern
Here's a complete example combining approaches:
spec.Operation<string, ApiResult<Order>>("GetOrder", (orderId, state) =>
{
if (!state.Orders.TryGetValue(orderId, out var expectedOrder))
{
return Expect.That<ApiResult<Order>>(r => r.IsNotFound,
"Order should not exist")
.SameState();
}
return Expect.That<ApiResult<Order>>(response =>
{
// First check status code
if (!response.IsSuccess)
return ValidationResult.Invalid(
$"Expected success, got {response.StatusCode}");
if (response.Data == null)
return ValidationResult.Invalid("Response data was null");
// Then check the payload with FluentAssertions
try
{
response.Data.Should().BeEquivalentTo(new Order
{
OrderId = orderId,
CustomerId = expectedOrder.CustomerId,
Status = expectedOrder.Status,
Items = expectedOrder.Items.Select(i => new OrderItem
{
ProductId = i.ProductId,
Quantity = i.Quantity,
Price = i.Price
}).ToList()
}, options => options
.Excluding(x => x.CreatedAt)
.Excluding(x => x.LastModified));
return ValidationResult.Valid();
}
catch (Exception ex)
{
return ValidationResult.Invalid(ex.Message);
}
}, $"Should return order {orderId}")
.SameState();
});
See Also
- Operations and Expect — Full details on the Expect API
- FluentAssertions Documentation