Skip to content

Commit 9b61daa

Browse files
authored
Update documentation SynchronizationContext in FakeTimeProvider (#5665)
* Update documentation SynchronizationContext in FakeTimeProvider * Fix lint error * Update documentation
1 parent 8b9dc1d commit 9b61daa

2 files changed

Lines changed: 21 additions & 28 deletions

File tree

src/Libraries/Microsoft.Extensions.TimeProvider.Testing/README.md

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,18 @@ timeProvider.Advance(TimeSpan.FromSeconds(5));
4242
myComponent.CheckState();
4343
```
4444

45-
## Use ConfigureAwait(true) with FakeTimeProvider.Advance
45+
## SynchronizationContext in xUnit Tests
4646

47-
The Advance method is used to simulate the passage of time. This can be useful in tests where you need to control the timing of asynchronous operations.
48-
When awaiting a task in a test that uses `FakeTimeProvider`, it's important to use `ConfigureAwait(true)`.
47+
### xUnit v2
4948

50-
Here's an example:
49+
Some testing libraries such as xUnit v2 provide custom `SynchronizationContext` for running tests. xUnit v2, for instance, provides `AsyncTestSyncContext` that allows to properly manage asynchronous operations withing the test execution. However, it brings an issue when we test asynchronous code that uses `ConfigureAwait(false)` in combination with class like `FakeTimeProvider`. In such cases, the xUnit context may lose track of the continuation, causing the test to become unresponsive, whether the test itself is asynchronous or not.
5150

52-
```cs
53-
await provider.Delay(TimeSpan.FromSeconds(delay)).ConfigureAwait(true);
51+
To prevent this issue, remove the xUnit context for tests dependent on `FakeTimeProvider` by setting the synchronization context to `null`:
52+
```
53+
SynchronizationContext.SetSynchronizationContext(null)
5454
```
5555

56-
This ensures that the continuation of the awaited task (i.e., the code that comes after the await statement) runs in the original context.
56+
The `Advance` method is used to simulate the passage of time. Below is an example how to create a test for a code that uses `ConfigureAwait(false)` that ensures that the continuation of the awaited task (i.e., the code that comes after the await statement) works correctly.
5757

5858
For a more realistic example, consider the following test using Polly:
5959

@@ -79,35 +79,21 @@ public class SomeService(TimeProvider timeProvider)
7979

8080
public async Task<int> PollyRetry(double taskDelay, double cancellationSeconds)
8181
{
82-
CancellationTokenSource cts = new(TimeSpan.FromSeconds(cancellationSeconds), timeProvider);
8382
Tries = 0;
84-
85-
// get a context from the pool and return it when done
86-
var context = ResilienceContextPool.Shared.Get(
87-
// ensure execution continues on captured context
88-
continueOnCapturedContext: true,
89-
cancellationToken: cts.Token);
90-
91-
var result = await _retryPipeline.ExecuteAsync(
83+
return await _retryPipeline.ExecuteAsync(
9284
async _ =>
9385
{
9486
Tries++;
95-
9687
// Simulate a task that takes some time to complete
97-
await Task.Delay(TimeSpan.FromSeconds(taskDelay), timeProvider).ConfigureAwait(true);
98-
99-
if (Tries <= 2)
88+
// With xUnit Context this would fail.
89+
await timeProvider.Delay(TimeSpan.FromSeconds(taskDelay)).ConfigureAwait(false);
90+
if (Tries < 2)
10091
{
10192
throw new InvalidOperationException();
10293
}
103-
10494
return Tries;
10595
},
106-
context);
107-
108-
ResilienceContextPool.Shared.Return(context);
109-
110-
return result;
96+
CancellationToken.None);
11197
}
11298
}
11399

@@ -118,6 +104,9 @@ public class SomeServiceTests
118104
[Fact]
119105
public void PollyRetry_ShouldHave2Tries()
120106
{
107+
// Arrange
108+
// Remove xUnit Context for this test
109+
SynchronizationContext.SetSynchronizationContext(null);
121110
var timeProvider = new FakeTimeProvider();
122111
var someService = new SomeService(timeProvider);
123112

@@ -138,6 +127,10 @@ public class SomeServiceTests
138127
}
139128
```
140129

130+
### xUnit v3
131+
132+
`AsyncTestSyncContext` has been removed more [here](https://xunit.net/docs/getting-started/v3/migration) so described issue is no longer a problem.
133+
141134
## Feedback & Contributing
142135

143136
We welcome feedback and contributions in [our GitHub repo](https://github.com/dotnet/extensions).

test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Collections.Generic;
66
using System.Threading;
77
using System.Threading.Tasks;
8-
using Microsoft.Extensions.Time.Testing;
98
using Xunit;
109

1110
namespace Microsoft.Extensions.Time.Testing.Test;
@@ -442,6 +441,7 @@ public void ShouldResetGateUnderLock_PreventingContextSwitching_AffectionOnTimer
442441
public void SimulateRetryPolicy()
443442
{
444443
// Arrange
444+
SynchronizationContext.SetSynchronizationContext(null);
445445
var retries = 42;
446446
var tries = 0;
447447
var taskDelay = 0.5;
@@ -469,7 +469,7 @@ async Task<int> simulatedPollyRetry()
469469
catch (InvalidOperationException)
470470
{
471471
// ConfigureAwait(true) is required to ensure that tasks continue on the captured context
472-
await provider.Delay(TimeSpan.FromSeconds(delay)).ConfigureAwait(true);
472+
await provider.Delay(TimeSpan.FromSeconds(delay)).ConfigureAwait(false);
473473
}
474474
}
475475
}

0 commit comments

Comments
 (0)