Skip to content

Commit ff4e3a9

Browse files
authored
feat: add WaitsFor assertion tests to validate resolved values and chaining capabilities (#3587)
1 parent f5a8cd8 commit ff4e3a9

6 files changed

+150
-0
lines changed

TUnit.Assertions.Tests/WaitsForAssertionTests.cs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,4 +262,129 @@ await Assert.That(getValue).WaitsFor(
262262
// Should complete in a reasonable time (well under 5 seconds)
263263
await Assert.That(stopwatch.Elapsed).IsLessThan(TimeSpan.FromSeconds(2));
264264
}
265+
266+
[Test]
267+
public async Task WaitsFor_Returns_Resolved_Value()
268+
{
269+
// Test that WaitsFor returns the value after assertion passes
270+
var value = 42;
271+
272+
var result = await Assert.That(value).WaitsFor(
273+
assert => assert.IsEqualTo(42),
274+
timeout: TimeSpan.FromSeconds(1));
275+
276+
await Assert.That(result).IsEqualTo(42);
277+
}
278+
279+
[Test]
280+
public async Task WaitsFor_Returns_Final_Polled_Value()
281+
{
282+
var counter = 0;
283+
Func<int> getValue = () => Interlocked.Increment(ref counter);
284+
285+
// Wait until counter reaches 5
286+
var result = await Assert.That(getValue).WaitsFor(
287+
assert => assert.IsGreaterThanOrEqualTo(5),
288+
timeout: TimeSpan.FromSeconds(5),
289+
pollingInterval: TimeSpan.FromMilliseconds(10));
290+
291+
// The returned value should be at least 5 (the value when assertion passed)
292+
await Assert.That(result).IsGreaterThanOrEqualTo(5);
293+
}
294+
295+
[Test]
296+
public async Task WaitsFor_Can_Chain_Assertions_On_Returned_Value()
297+
{
298+
var value = 42;
299+
300+
// Capture the result and perform additional assertions
301+
var result = await Assert.That(value).WaitsFor(
302+
assert => assert.IsGreaterThan(40),
303+
timeout: TimeSpan.FromSeconds(1));
304+
305+
// Can perform further assertions on the captured value
306+
await Assert.That(result).IsLessThan(50);
307+
await Assert.That(result).IsEqualTo(42);
308+
}
309+
310+
[Test]
311+
public async Task WaitsFor_Works_With_Complex_Object()
312+
{
313+
// Simulate the real-world scenario from the GitHub issue
314+
var entity = new TestEntity { Id = 1, Name = "Test", IsReady = false };
315+
316+
// Simulate async state change
317+
_ = Task.Run(async () =>
318+
{
319+
await Task.Delay(50);
320+
entity.IsReady = true;
321+
});
322+
323+
// Wait for entity to be ready and capture it
324+
var result = await Assert.That(() => entity).WaitsFor(
325+
assert => assert.Satisfies(e => e?.IsReady == true),
326+
timeout: TimeSpan.FromSeconds(2),
327+
pollingInterval: TimeSpan.FromMilliseconds(10));
328+
329+
// Verify we got the entity back
330+
await Assert.That(result).IsNotNull();
331+
await Assert.That(result!.Id).IsEqualTo(1);
332+
await Assert.That(result.Name).IsEqualTo("Test");
333+
await Assert.That(result.IsReady).IsEqualTo(true);
334+
}
335+
336+
[Test]
337+
public async Task WaitsFor_Returns_Null_When_Value_Is_Null()
338+
{
339+
string? nullValue = null;
340+
341+
var result = await Assert.That(() => nullValue).WaitsFor(
342+
assert => assert.IsNull(),
343+
timeout: TimeSpan.FromSeconds(1));
344+
345+
await Assert.That(result).IsNull();
346+
}
347+
348+
[Test]
349+
public async Task WaitsFor_Can_Be_Used_In_Multiple_Assertion_Block()
350+
{
351+
var entity = new TestEntity { Id = 42, Name = "Sample", IsReady = true };
352+
353+
var result = await Assert.That(() => entity).WaitsFor(
354+
assert => assert.IsNotNull(),
355+
timeout: TimeSpan.FromSeconds(1));
356+
357+
using (Assert.Multiple())
358+
{
359+
await Assert.That(result).IsNotNull();
360+
await Assert.That(result!.Id).IsEqualTo(42);
361+
await Assert.That(result.Name).IsEqualTo("Sample");
362+
await Assert.That(result.IsReady).IsEqualTo(true);
363+
}
364+
}
365+
366+
[Test]
367+
public async Task WaitsFor_GitHub_Issue_Example_Scenario()
368+
{
369+
// This is the exact scenario from GitHub issue #3585
370+
Func<TestEntity?> getEntity = () => new TestEntity { Id = 100, Name = "Entity", IsReady = true };
371+
372+
TestEntity? entity = await Assert.That(getEntity)
373+
.WaitsFor(e => e.IsNotNull(), TimeSpan.FromSeconds(15));
374+
375+
using (Assert.Multiple())
376+
{
377+
await Assert.That(entity).IsNotNull();
378+
await Assert.That(entity!.Id).IsEqualTo(100);
379+
await Assert.That(entity.Name).IsEqualTo("Entity");
380+
}
381+
}
382+
383+
// Helper class for testing complex objects
384+
private class TestEntity
385+
{
386+
public int Id { get; set; }
387+
public string Name { get; set; } = string.Empty;
388+
public bool IsReady { get; set; }
389+
}
265390
}

TUnit.Assertions/Conditions/WaitsForAssertion.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ protected override async Task<AssertionResult> CheckAsync(EvaluationMetadata<TVa
5757
var assertion = _assertionBuilder(assertionSource);
5858
await assertion.AssertAsync();
5959

60+
// Store the successfully resolved value so it can be returned
61+
_resolvedValue = currentValue;
6062
return AssertionResult.Passed;
6163
}
6264
catch (AssertionException ex)
@@ -90,6 +92,25 @@ protected override async Task<AssertionResult> CheckAsync(EvaluationMetadata<TVa
9092
$"assertion did not pass within {_timeout.TotalMilliseconds:F0}ms after {attemptCount} attempts. {lastErrorMessage}");
9193
}
9294

95+
/// <summary>
96+
/// The resolved value after the assertion passes.
97+
/// This allows users to capture and use the value in downstream assertions.
98+
/// </summary>
99+
private TValue? _resolvedValue;
100+
101+
/// <summary>
102+
/// Executes the assertion and returns the resolved value upon success.
103+
/// This enables the pattern: Entity entity = await Assert.That(getEntity).WaitsFor(...);
104+
/// </summary>
105+
public override async Task<TValue?> AssertAsync()
106+
{
107+
// Execute the base assertion logic (which calls CheckAsync and handles the result)
108+
await base.AssertAsync();
109+
110+
// Return the resolved value that was stored when the assertion passed
111+
return _resolvedValue;
112+
}
113+
93114
protected override string GetExpectation() =>
94115
$"assertion to pass within {_timeout.TotalMilliseconds:F0} milliseconds " +
95116
$"(polling every {_pollingInterval.TotalMilliseconds:F0}ms)";

TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1680,6 +1680,7 @@ namespace .Conditions
16801680
public class WaitsForAssertion<TValue> : .<TValue>
16811681
{
16821682
public WaitsForAssertion(.<TValue> context, <.<TValue>, .<TValue>> assertionBuilder, timeout, ? pollingInterval = default) { }
1683+
public override .<TValue?> AssertAsync() { }
16831684
protected override .<.> CheckAsync(.<TValue> metadata) { }
16841685
protected override string GetExpectation() { }
16851686
}

TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,6 +1677,7 @@ namespace .Conditions
16771677
public class WaitsForAssertion<TValue> : .<TValue>
16781678
{
16791679
public WaitsForAssertion(.<TValue> context, <.<TValue>, .<TValue>> assertionBuilder, timeout, ? pollingInterval = default) { }
1680+
public override .<TValue?> AssertAsync() { }
16801681
protected override .<.> CheckAsync(.<TValue> metadata) { }
16811682
protected override string GetExpectation() { }
16821683
}

TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1680,6 +1680,7 @@ namespace .Conditions
16801680
public class WaitsForAssertion<TValue> : .<TValue>
16811681
{
16821682
public WaitsForAssertion(.<TValue> context, <.<TValue>, .<TValue>> assertionBuilder, timeout, ? pollingInterval = default) { }
1683+
public override .<TValue?> AssertAsync() { }
16831684
protected override .<.> CheckAsync(.<TValue> metadata) { }
16841685
protected override string GetExpectation() { }
16851686
}

TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1533,6 +1533,7 @@ namespace .Conditions
15331533
public class WaitsForAssertion<TValue> : .<TValue>
15341534
{
15351535
public WaitsForAssertion(.<TValue> context, <.<TValue>, .<TValue>> assertionBuilder, timeout, ? pollingInterval = default) { }
1536+
public override .<TValue?> AssertAsync() { }
15361537
protected override .<.> CheckAsync(.<TValue> metadata) { }
15371538
protected override string GetExpectation() { }
15381539
}

0 commit comments

Comments
 (0)