Skip to content

Commit 104686d

Browse files
committed
feat: Add IAsyncDiposable for TestContext
1 parent 4d859bd commit 104686d

File tree

3 files changed

+44
-22
lines changed

3 files changed

+44
-22
lines changed

MIGRATION.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,7 @@ To make the API more consistent, `RenderComponent` and `SetParametersAndRender`
4949

5050
## Removal of `ComponentParameter` and method using them
5151
Using `ComponentParameter` and factory methods to create them is not recommend in V1 and have now been removed in V2. Instead, use the strongly typed builder pattern that enables you to pass parameters to components you render.
52+
53+
## `TestContext` implements `IDisposable` and `IAsyncDisposable`
54+
The `TestContext` now implements `IDisposable` and `IAsyncDisposable`. In version 1.x, `TestContext` only implemented `IDisposable` and cleaned up asynchronous objects in the synchronous `Dispose` method. This is no longer the case, and asynchronous objects are now cleaned up in the `DisposeAsync` method.
55+
If you register services into the container that implement `IAsyncDisposable` make sure that the test framework calls the right method.

src/bunit/TestContext.cs

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Bunit;
77
/// <summary>
88
/// A test context is a factory that makes it possible to create components under tests.
99
/// </summary>
10-
public partial class TestContext : IDisposable
10+
public partial class TestContext : IDisposable, IAsyncDisposable
1111
{
1212
private bool disposed;
1313
private BunitRenderer? bunitRenderer;
@@ -70,17 +70,46 @@ public void Dispose()
7070
GC.SuppressFinalize(this);
7171
}
7272

73+
/// <inheritdoc/>
74+
public async ValueTask DisposeAsync()
75+
{
76+
await DisposeAsyncCore();
77+
78+
Dispose(disposing: false);
79+
GC.SuppressFinalize(this);
80+
}
81+
82+
/// <summary>
83+
/// Disposes of the test context resources that are asynchronous, in particular it disposes the <see cref="Services"/>
84+
/// service provider.s
85+
/// </summary>
86+
protected virtual async ValueTask DisposeAsyncCore()
87+
{
88+
if (disposed)
89+
return;
90+
91+
disposed = true;
92+
93+
// Ensure the renderer is disposed before all others,
94+
// otherwise a render cycle may be ongoing and try to access
95+
// the service provider to perform operations.
96+
if (bunitRenderer is not null)
97+
{
98+
await bunitRenderer.DisposeAsync();
99+
}
100+
101+
await Services.DisposeAsync();
102+
}
103+
73104
/// <summary>
74105
/// Disposes of the test context resources, in particular it disposes the <see cref="Services"/>
75-
/// service provider. Any async services registered with the service provider will disposed first,
76-
/// but their disposal will not be awaited..
106+
/// service provider.
77107
/// </summary>
78108
/// <remarks>
79109
/// The disposing parameter should be false when called from a finalizer, and true when called from the
80110
/// <see cref="Dispose()"/> method. In other words, it is true when deterministically called and false when non-deterministically called.
81111
/// </remarks>
82112
/// <param name="disposing">Set to true if called from <see cref="Dispose()"/>, false if called from a finalizer.f.</param>
83-
[SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly", Justification = "Explicitly ignoring DisposeAsync to avoid breaking changes to API surface.")]
84113
protected virtual void Dispose(bool disposing)
85114
{
86115
if (disposed || !disposing)
@@ -91,18 +120,7 @@ protected virtual void Dispose(bool disposing)
91120
// Ensure the renderer is disposed before all others,
92121
// otherwise a render cycle may be ongoing and try to access
93122
// the service provider to perform operations.
94-
if (bunitRenderer is IDisposable renderer)
95-
{
96-
renderer.Dispose();
97-
}
98-
99-
// Ignore the async task as GetAwaiter().GetResult() can cause deadlock
100-
// and implementing IAsyncDisposable in TestContext will be a breaking change.
101-
//
102-
// NOTE: This has to be called before Services.Dispose().
103-
// If there are IAsyncDisposable services registered, calling Dispose first
104-
// causes the service provider to throw an exception.
105-
_ = Services.DisposeAsync();
123+
bunitRenderer?.Dispose();
106124

107125
// The service provider should dispose of any
108126
// disposable object it has created, when it is disposed.

tests/bunit.tests/TestContextTest.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,10 @@ public void Test203()
207207
}
208208

209209
[Fact(DisplayName = "Can correctly resolve and dispose of scoped disposable service")]
210-
public void Net5Test001()
210+
public async Task Net5Test001()
211211
{
212212
AsyncDisposableService asyncDisposable;
213-
using (var sut = new TestContext())
213+
await using (var sut = new TestContext())
214214
{
215215
sut.Services.AddScoped<AsyncDisposableService>();
216216
asyncDisposable = sut.Services.GetService<AsyncDisposableService>();
@@ -219,10 +219,10 @@ public void Net5Test001()
219219
}
220220

221221
[Fact(DisplayName = "Can correctly resolve and dispose of transient disposable service")]
222-
public void Net5Test002()
222+
public async Task Net5Test002()
223223
{
224224
AsyncDisposableService asyncDisposable;
225-
using (var sut = new TestContext())
225+
await using (var sut = new TestContext())
226226
{
227227
sut.Services.AddTransient<AsyncDisposableService>();
228228
asyncDisposable = sut.Services.GetService<AsyncDisposableService>();
@@ -231,10 +231,10 @@ public void Net5Test002()
231231
}
232232

233233
[Fact(DisplayName = "Can correctly resolve and dispose of singleton disposable service")]
234-
public void Net5Test003()
234+
public async Task Net5Test003()
235235
{
236236
AsyncDisposableService asyncDisposable;
237-
using (var sut = new TestContext())
237+
await using (var sut = new TestContext())
238238
{
239239
sut.Services.AddSingleton<AsyncDisposableService>();
240240
asyncDisposable = sut.Services.GetService<AsyncDisposableService>();

0 commit comments

Comments
 (0)