books/backend/Books.Api.Tests/Helpers/Eventually.cs
Nicolaj Hartmann 66f6fa138d Initial commit: Books accounting system with EventFlow CQRS
Backend (.NET 10):
- EventFlow CQRS/Event Sourcing with PostgreSQL
- GraphQL.NET API with mutations and queries
- Custom ReadModelSqlGenerator for snake_case PostgreSQL columns
- Hangfire for background job processing
- Integration tests with isolated test databases

Frontend (React/Vite):
- Initial project structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 02:52:30 +01:00

89 lines
2.6 KiB
C#

namespace Books.Api.Tests.Helpers;
/// <summary>
/// Helper for testing eventually consistent systems.
/// Polls a condition until it's met or times out.
/// </summary>
public static class Eventually
{
/// <summary>
/// Polls a condition until it returns true or times out.
/// </summary>
public static async Task AssertAsync(
Func<Task<bool>> condition,
TimeSpan? timeout = null,
TimeSpan? pollInterval = null,
string? failMessage = null)
{
timeout ??= TimeSpan.FromSeconds(5);
pollInterval ??= TimeSpan.FromMilliseconds(50);
var start = DateTime.UtcNow;
while (DateTime.UtcNow - start < timeout)
{
if (await condition())
return;
await Task.Delay(pollInterval.Value);
}
throw new TimeoutException(
failMessage ?? $"Condition was not met within {timeout}.");
}
/// <summary>
/// Polls until a non-null result is returned or times out.
/// </summary>
public static async Task<T> GetAsync<T>(
Func<Task<T?>> getter,
TimeSpan? timeout = null,
TimeSpan? pollInterval = null,
string? failMessage = null) where T : class
{
timeout ??= TimeSpan.FromSeconds(10);
pollInterval ??= TimeSpan.FromMilliseconds(100);
var start = DateTime.UtcNow;
while (DateTime.UtcNow - start < timeout)
{
var result = await getter();
if (result != null)
return result;
await Task.Delay(pollInterval.Value);
}
throw new TimeoutException(
failMessage ?? $"Expected non-null result was not returned within {timeout}.");
}
/// <summary>
/// Polls until a collection has the expected count or times out.
/// </summary>
public static async Task<IReadOnlyList<T>> GetListAsync<T>(
Func<Task<IReadOnlyList<T>>> getter,
int expectedCount,
TimeSpan? timeout = null,
TimeSpan? pollInterval = null,
string? failMessage = null)
{
timeout ??= TimeSpan.FromSeconds(10);
pollInterval ??= TimeSpan.FromMilliseconds(100);
var start = DateTime.UtcNow;
while (DateTime.UtcNow - start < timeout)
{
var result = await getter();
if (result.Count >= expectedCount)
return result;
await Task.Delay(pollInterval.Value);
}
throw new TimeoutException(
failMessage ?? $"Expected {expectedCount} items but condition was not met within {timeout}.");
}
}