Skip to content

Commit 3eb8c8e

Browse files
committed
ITestDbContextProvider is IAsyncDisposable
1 parent 361e1c2 commit 3eb8c8e

File tree

6 files changed

+460
-257
lines changed

6 files changed

+460
-257
lines changed

src/Thinktecture.EntityFrameworkCore.SqlServer.Testing/EntityFrameworkCore/Testing/ITestIsolationOptions.cs

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,43 @@ namespace Thinktecture.EntityFrameworkCore.Testing;
1010
/// </summary>
1111
public interface ITestIsolationOptions
1212
{
13+
/// <summary>
14+
/// No test isolation, no cleanup.
15+
/// </summary>
16+
public static readonly ITestIsolationOptions None = new NoCleanup();
17+
1318
/// <summary>
1419
/// Test isolation via ambient transaction.
1520
/// </summary>
1621
public static readonly ITestIsolationOptions SharedTablesAmbientTransaction = new ShareTablesIsolation();
1722

1823
/// <summary>
19-
/// Rollbacks migrations and then deletes database objects (like tables).
24+
/// Rollbacks migrations and then deletes database objects (like tables) with a schema used by the tests.
2025
/// </summary>
2126
public static readonly ITestIsolationOptions RollbackMigrationsAndCleanup = new RollbackMigrationsAndCleanupDatabase();
2227

2328
/// <summary>
24-
/// Deletes database objects (like tables).
29+
/// Deletes database objects (like tables) with a schema used by the tests.
2530
/// </summary>
2631
public static readonly ITestIsolationOptions CleanupOnly = new CleanupDatabase();
2732

33+
/// <summary>
34+
/// Deletes all records from the tables.
35+
/// </summary>
36+
public static readonly ITestIsolationOptions TruncateTables = new TruncateAllTables();
37+
38+
/// <summary>
39+
/// Performs custom cleanup.
40+
/// </summary>
41+
/// <param name="cleanup">Callback that performs the actual cleanup.</param>
42+
/// <typeparam name="T">Type of the <see cref="DbContext"/></typeparam>
43+
/// <returns></returns>
44+
public static ITestIsolationOptions Custom<T>(Func<T, string, CancellationToken, Task> cleanup)
45+
where T : DbContext
46+
{
47+
return new CustomCleanup<T>(cleanup);
48+
}
49+
2850
/// <summary>
2951
/// Indicator, whether the database needs cleanup.
3052
/// </summary>
@@ -33,60 +55,103 @@ public interface ITestIsolationOptions
3355
/// <summary>
3456
/// Cleanup of the database.
3557
/// </summary>
36-
void Cleanup(DbContext dbContext, string schema);
58+
ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationToken cancellationToken);
59+
60+
private class NoCleanup : ITestIsolationOptions
61+
{
62+
public bool NeedsCleanup => false;
63+
64+
public ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationToken cancellationToken)
65+
{
66+
return ValueTask.CompletedTask;
67+
}
68+
}
3769

3870
private class ShareTablesIsolation : ITestIsolationOptions
3971
{
4072
public bool NeedsCleanup => false;
4173

42-
public void Cleanup(DbContext dbContext, string schema)
74+
public ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationToken cancellationToken)
4375
{
76+
return ValueTask.CompletedTask;
4477
}
4578
}
4679

4780
private class RollbackMigrationsAndCleanupDatabase : ITestIsolationOptions
4881
{
4982
public bool NeedsCleanup => true;
5083

51-
public void Cleanup(DbContext dbContext, string schema)
84+
public async ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationToken cancellationToken)
5285
{
53-
RollbackMigrations(dbContext);
54-
DeleteDatabaseObjects(dbContext, schema);
86+
await RollbackMigrationsAsync(dbContext, cancellationToken);
87+
await DeleteDatabaseObjectsAsync(dbContext, schema, cancellationToken);
5588
}
5689
}
5790

5891
private class CleanupDatabase : ITestIsolationOptions
5992
{
6093
public bool NeedsCleanup => true;
6194

62-
public void Cleanup(DbContext dbContext, string schema)
95+
public async ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationToken cancellationToken)
96+
{
97+
await DeleteDatabaseObjectsAsync(dbContext, schema, cancellationToken);
98+
}
99+
}
100+
101+
private class CustomCleanup<T> : ITestIsolationOptions
102+
where T : DbContext
103+
{
104+
private readonly Func<T, string, CancellationToken, Task> _cleanup;
105+
106+
public bool NeedsCleanup => true;
107+
108+
public CustomCleanup(Func<T, string, CancellationToken, Task> cleanup)
109+
{
110+
_cleanup = cleanup;
111+
}
112+
113+
public async ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationToken cancellationToken)
114+
{
115+
await _cleanup((T)dbContext, schema, cancellationToken);
116+
}
117+
}
118+
119+
private class TruncateAllTables : ITestIsolationOptions
120+
{
121+
public bool NeedsCleanup => true;
122+
123+
public async ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationToken cancellationToken)
63124
{
64-
DeleteDatabaseObjects(dbContext, schema);
125+
foreach (var entityType in dbContext.Model.GetEntityTypes())
126+
{
127+
if (entityType.GetTableName() is not null)
128+
await dbContext.TruncateTableAsync(entityType.ClrType, cancellationToken);
129+
}
65130
}
66131
}
67132

68133
/// <summary>
69134
/// Rollbacks all migrations.
70135
/// </summary>
71-
protected static void RollbackMigrations(DbContext dbContext)
136+
protected static async Task RollbackMigrationsAsync(DbContext dbContext, CancellationToken cancellationToken)
72137
{
73138
ArgumentNullException.ThrowIfNull(dbContext);
74139

75-
dbContext.GetService<IMigrator>().Migrate("0");
140+
await dbContext.GetService<IMigrator>().MigrateAsync("0", cancellationToken);
76141
}
77142

78143
/// <summary>
79144
/// Deletes database objects (like tables) with provided <paramref name="schema"/>.
80145
/// </summary>
81-
protected static void DeleteDatabaseObjects(DbContext ctx, string schema)
146+
protected static async Task DeleteDatabaseObjectsAsync(DbContext ctx, string schema, CancellationToken cancellationToken)
82147
{
83148
ArgumentNullException.ThrowIfNull(ctx);
84149
ArgumentNullException.ThrowIfNull(schema);
85150

86151
var sqlHelper = ctx.GetService<ISqlGenerationHelper>();
87152

88-
ctx.Database.ExecuteSqlRaw(GetSqlForCleanup(), new SqlParameter("@schema", schema));
89-
ctx.Database.ExecuteSqlRaw(GetDropSchemaSql(sqlHelper, schema));
153+
await ctx.Database.ExecuteSqlRawAsync(GetSqlForCleanup(), new object[] { new SqlParameter("@schema", schema) }, cancellationToken);
154+
await ctx.Database.ExecuteSqlRawAsync(GetDropSchemaSql(sqlHelper, schema), cancellationToken);
90155
}
91156

92157
private static string GetSqlForCleanup()

src/Thinktecture.EntityFrameworkCore.SqlServer.Testing/EntityFrameworkCore/Testing/SqlServerDbContextIntegrationTests.cs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Xunit;
12
using Xunit.Abstractions;
23

34
namespace Thinktecture.EntityFrameworkCore.Testing;
@@ -6,7 +7,7 @@ namespace Thinktecture.EntityFrameworkCore.Testing;
67
/// A base class for integration tests using EF Core along with SQL Server.
78
/// </summary>
89
/// <typeparam name="T">Type of the database context.</typeparam>
9-
public abstract class SqlServerDbContextIntegrationTests<T> : ITestDbContextProvider<T>
10+
public abstract class SqlServerDbContextIntegrationTests<T> : ITestDbContextProvider<T>, IAsyncLifetime
1011
where T : DbContext
1112
{
1213
private SqlServerTestDbContextProvider<T>? _testCtxProvider;
@@ -16,8 +17,9 @@ public abstract class SqlServerDbContextIntegrationTests<T> : ITestDbContextProv
1617
/// </summary>
1718
protected SqlServerTestDbContextProvider<T> TestCtxProvider => _testCtxProvider ??= TestCtxProviderBuilder.Build();
1819

19-
private bool _isProviderConfigured;
2020
private readonly SqlServerTestDbContextProviderBuilder<T> _testCtxProviderBuilder;
21+
private bool _isDisposed;
22+
private bool _isProviderConfigured;
2123

2224
/// <summary>
2325
/// Gets the <see cref="SqlServerTestDbContextProviderBuilder{T}"/> which is created on the first access.
@@ -97,23 +99,61 @@ public T CreateDbContext(bool useMasterConnection)
9799
return TestCtxProvider.CreateDbContext(useMasterConnection);
98100
}
99101

102+
/// <inheritdoc />
103+
public virtual Task InitializeAsync()
104+
{
105+
return Task.CompletedTask;
106+
}
107+
100108
/// <inheritdoc />
101109
public void Dispose()
102110
{
111+
if (_isDisposed)
112+
return;
113+
114+
_isDisposed = true;
115+
103116
Dispose(true);
104117
GC.SuppressFinalize(this);
105118
}
106119

120+
/// <inheritdoc />
121+
async Task IAsyncLifetime.DisposeAsync()
122+
{
123+
await DisposeAsync();
124+
}
125+
126+
/// <inheritdoc />
127+
public async ValueTask DisposeAsync()
128+
{
129+
if (_isDisposed)
130+
return;
131+
132+
_isDisposed = true;
133+
134+
await DisposeAsync(true);
135+
GC.SuppressFinalize(this);
136+
}
137+
107138
/// <summary>
108139
/// Disposes of managed resources like the <see cref="SqlServerTestDbContextProvider{T}"/>.
109140
/// </summary>
110141
/// <param name="disposing">Indication that the method is being called by <see cref="Dispose()"/>.</param>
111142
protected virtual void Dispose(bool disposing)
143+
{
144+
DisposeAsync(disposing).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
145+
}
146+
147+
/// <summary>
148+
/// Disposes of managed resources like the <see cref="SqlServerTestDbContextProvider{T}"/>.
149+
/// </summary>
150+
/// <param name="disposing">Indication that the method is being called by <see cref="Dispose()"/>.</param>
151+
protected virtual async ValueTask DisposeAsync(bool disposing)
112152
{
113153
if (!disposing)
114154
return;
115155

116-
_testCtxProvider?.Dispose();
156+
await (_testCtxProvider?.DisposeAsync() ?? ValueTask.CompletedTask);
117157
_testCtxProvider = null;
118158
}
119159
}

src/Thinktecture.EntityFrameworkCore.SqlServer.Testing/EntityFrameworkCore/Testing/SqlServerTestDbContextProvider.cs

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class SqlServerTestDbContextProvider<T> : ITestDbContextProvider<T>
3232
private IDbContextTransaction? _tx;
3333
private bool _isAtLeastOneContextCreated;
3434
private readonly IsolationLevel _sharedTablesIsolationLevel;
35+
private bool _isDisposed;
3536

3637
/// <inheritdoc />
3738
public T ArrangeDbContext => _arrangeDbContext ??= CreateDbContext(true);
@@ -214,55 +215,84 @@ protected virtual void RunMigrations(T ctx)
214215

215216
/// <summary>
216217
/// Rollbacks transaction if shared tables are used
217-
/// otherwise the migrations are rolled back and all tables, functions, views and the newly generated schema are deleted.
218+
/// otherwise performs cleanup according to provided <see cref="ITestIsolationOptions"/>.
218219
/// </summary>
219220
public void Dispose()
220221
{
222+
if (_isDisposed)
223+
return;
224+
225+
_isDisposed = true;
226+
221227
Dispose(true);
222228
GC.SuppressFinalize(this);
223229
}
224230

231+
/// <summary>
232+
/// Rollbacks transaction if shared tables are used
233+
/// otherwise performs cleanup according to provided <see cref="ITestIsolationOptions"/>.
234+
/// </summary>
235+
public async ValueTask DisposeAsync()
236+
{
237+
if (_isDisposed)
238+
return;
239+
240+
_isDisposed = true;
241+
242+
await DisposeAsync(true);
243+
GC.SuppressFinalize(this);
244+
}
245+
225246
/// <summary>
226247
/// Disposes of inner resources.
227248
/// </summary>
228249
/// <param name="disposing">Indication whether this method is being called by the method <see cref="SqlServerTestDbContextProvider{T}.Dispose()"/>.</param>
229250
protected virtual void Dispose(bool disposing)
251+
{
252+
DisposeAsync(disposing).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
253+
}
254+
255+
/// <summary>
256+
/// Disposes of inner resources.
257+
/// </summary>
258+
/// <param name="disposing">Indication whether this method is being called by the method <see cref="SqlServerTestDbContextProvider{T}.Dispose()"/>.</param>
259+
protected virtual async ValueTask DisposeAsync(bool disposing)
230260
{
231261
if (!disposing)
232262
return;
233263

234264
if (_isAtLeastOneContextCreated)
235265
{
236-
DisposeContextsAndRollbackMigrations();
266+
await DisposeContextsAndRollbackMigrationsAsync(default);
237267
_isAtLeastOneContextCreated = false;
238268
}
239269

240270
_masterConnection.Dispose();
241271
_testingLoggingOptions.Dispose();
242272
}
243273

244-
private void DisposeContextsAndRollbackMigrations()
274+
private async Task DisposeContextsAndRollbackMigrationsAsync(CancellationToken cancellationToken)
245275
{
246-
_tx?.Rollback();
247-
_tx?.Dispose();
276+
if (_tx is not null)
277+
{
278+
await _tx.RollbackAsync(cancellationToken);
279+
await _tx.DisposeAsync();
280+
}
248281

249282
if (_isolationOptions.NeedsCleanup)
250283
{
251284
// Create a new ctx as a last resort to rollback migrations and clean up the database
252-
using var ctx = _actDbContext ?? _arrangeDbContext ?? _assertDbContext ?? CreateDbContext(_masterDbContextOptions, new DbDefaultSchema(Schema));
285+
await using var ctx = _actDbContext ?? _arrangeDbContext ?? _assertDbContext ?? CreateDbContext(_masterDbContextOptions, new DbDefaultSchema(Schema));
253286

254-
_isolationOptions.Cleanup(ctx, Schema);
287+
await _isolationOptions.CleanupAsync(ctx, Schema, cancellationToken);
255288
}
256289

257-
_arrangeDbContext?.Dispose();
258-
_actDbContext?.Dispose();
259-
_assertDbContext?.Dispose();
290+
await (_arrangeDbContext?.DisposeAsync() ?? ValueTask.CompletedTask);
291+
await (_actDbContext?.DisposeAsync() ?? ValueTask.CompletedTask);
292+
await (_assertDbContext?.DisposeAsync() ?? ValueTask.CompletedTask);
260293

261294
_arrangeDbContext = null;
262295
_actDbContext = null;
263296
_assertDbContext = null;
264297
}
265-
266-
267-
268298
}

0 commit comments

Comments
 (0)