Skip to content

Commit 1cd81ba

Browse files
committed
Reworked ITestIsolationOptions
1 parent ced35f3 commit 1cd81ba

File tree

9 files changed

+109
-57
lines changed

9 files changed

+109
-57
lines changed

azure-pipelines.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ variables:
1111
SourceBranchName: '$(Build.SourceBranchName)'
1212

1313
pool:
14-
vmImage: 'windows-latest'
14+
vmImage: 'ubuntu-latest'
1515

1616
steps:
1717

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

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,42 +11,57 @@ namespace Thinktecture.EntityFrameworkCore.Testing;
1111
public interface ITestIsolationOptions
1212
{
1313
/// <summary>
14-
/// No test isolation, no cleanup.
14+
/// No test isolation, i.e. no ambient transaction, no unique schema, no cleanup.
1515
/// </summary>
16-
public static readonly ITestIsolationOptions None = new NoCleanup();
16+
public static readonly ITestIsolationOptions None = new NoIsolation();
1717

1818
/// <summary>
1919
/// Test isolation via ambient transaction.
20+
/// No unique schema, no cleanup.
2021
/// </summary>
2122
public static readonly ITestIsolationOptions SharedTablesAmbientTransaction = new ShareTablesIsolation();
2223

2324
/// <summary>
2425
/// Rollbacks migrations and then deletes database objects (like tables) with a schema used by the tests.
26+
/// No ambient transaction; uses unique schema.
2527
/// </summary>
2628
public static readonly ITestIsolationOptions RollbackMigrationsAndCleanup = new RollbackMigrationsAndCleanupDatabase();
2729

2830
/// <summary>
2931
/// Deletes database objects (like tables) with a schema used by the tests.
32+
/// No ambient transaction; uses unique schema.
3033
/// </summary>
3134
public static readonly ITestIsolationOptions CleanupOnly = new CleanupDatabase();
3235

3336
/// <summary>
3437
/// Deletes all records from the tables.
38+
/// No ambient transaction; no unique schema.
3539
/// </summary>
3640
public static readonly ITestIsolationOptions TruncateTables = new TruncateAllTables();
3741

3842
/// <summary>
3943
/// Performs custom cleanup.
4044
/// </summary>
45+
/// <param name="needsUniqueSchema">Indicator whether the tables require an unique database schema.</param>
4146
/// <param name="cleanup">Callback that performs the actual cleanup.</param>
4247
/// <typeparam name="T">Type of the <see cref="DbContext"/></typeparam>
4348
/// <returns></returns>
44-
public static ITestIsolationOptions Custom<T>(Func<T, string, CancellationToken, Task> cleanup)
49+
public static ITestIsolationOptions Custom<T>(bool needsUniqueSchema, Func<T, string, CancellationToken, Task> cleanup)
4550
where T : DbContext
4651
{
47-
return new CustomCleanup<T>(cleanup);
52+
return new CustomCleanup<T>(needsUniqueSchema, cleanup);
4853
}
4954

55+
/// <summary>
56+
/// Indicator, whether the database needs cleanup.
57+
/// </summary>
58+
bool NeedsAmbientTransaction { get; }
59+
60+
/// <summary>
61+
/// Indicator, whether the database needs cleanup.
62+
/// </summary>
63+
bool NeedsUniqueSchema { get; }
64+
5065
/// <summary>
5166
/// Indicator, whether the database needs cleanup.
5267
/// </summary>
@@ -57,8 +72,10 @@ public static ITestIsolationOptions Custom<T>(Func<T, string, CancellationToken,
5772
/// </summary>
5873
ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationToken cancellationToken);
5974

60-
private class NoCleanup : ITestIsolationOptions
75+
private class NoIsolation : ITestIsolationOptions
6176
{
77+
public bool NeedsAmbientTransaction => false;
78+
public bool NeedsUniqueSchema => false;
6279
public bool NeedsCleanup => false;
6380

6481
public ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationToken cancellationToken)
@@ -69,6 +86,8 @@ public ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationTo
6986

7087
private class ShareTablesIsolation : ITestIsolationOptions
7188
{
89+
public bool NeedsAmbientTransaction => true;
90+
public bool NeedsUniqueSchema => false;
7291
public bool NeedsCleanup => false;
7392

7493
public ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationToken cancellationToken)
@@ -79,6 +98,8 @@ public ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationTo
7998

8099
private class RollbackMigrationsAndCleanupDatabase : ITestIsolationOptions
81100
{
101+
public bool NeedsAmbientTransaction => false;
102+
public bool NeedsUniqueSchema => true;
82103
public bool NeedsCleanup => true;
83104

84105
public async ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationToken cancellationToken)
@@ -90,6 +111,8 @@ public async ValueTask CleanupAsync(DbContext dbContext, string schema, Cancella
90111

91112
private class CleanupDatabase : ITestIsolationOptions
92113
{
114+
public bool NeedsAmbientTransaction => false;
115+
public bool NeedsUniqueSchema => true;
93116
public bool NeedsCleanup => true;
94117

95118
public async ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationToken cancellationToken)
@@ -103,10 +126,13 @@ private class CustomCleanup<T> : ITestIsolationOptions
103126
{
104127
private readonly Func<T, string, CancellationToken, Task> _cleanup;
105128

129+
public bool NeedsAmbientTransaction => false;
130+
public bool NeedsUniqueSchema { get; }
106131
public bool NeedsCleanup => true;
107132

108-
public CustomCleanup(Func<T, string, CancellationToken, Task> cleanup)
133+
public CustomCleanup(bool needsUniqueSchema, Func<T, string, CancellationToken, Task> cleanup)
109134
{
135+
NeedsUniqueSchema = needsUniqueSchema;
110136
_cleanup = cleanup;
111137
}
112138

@@ -118,6 +144,8 @@ public async ValueTask CleanupAsync(DbContext dbContext, string schema, Cancella
118144

119145
private class TruncateAllTables : ITestIsolationOptions
120146
{
147+
public bool NeedsAmbientTransaction => false;
148+
public bool NeedsUniqueSchema => false;
121149
public bool NeedsCleanup => true;
122150

123151
public async ValueTask CleanupAsync(DbContext dbContext, string schema, CancellationToken cancellationToken)

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

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System.Collections.Concurrent;
22
using System.Data;
33
using System.Data.Common;
4+
using System.Linq.Expressions;
5+
using System.Reflection;
46
using Microsoft.EntityFrameworkCore.Infrastructure;
57
using Microsoft.EntityFrameworkCore.Storage;
68
using Thinktecture.Logging;
@@ -26,6 +28,7 @@ public class SqlServerTestDbContextProvider<T> : ITestDbContextProvider<T>
2628
private readonly Func<DbContextOptions<T>, IDbDefaultSchema, T?>? _contextFactory;
2729
private readonly TestingLoggingOptions _testingLoggingOptions;
2830

31+
private Func<DbContextOptions<T>, IDbDefaultSchema, T>? _defaultContextFactory;
2932
private T? _arrangeDbContext;
3033
private T? _actDbContext;
3134
private T? _assertDbContext;
@@ -109,7 +112,7 @@ public T CreateDbContext()
109112
/// <returns>A new instance of <typeparamref name="T"/>.</returns>
110113
public T CreateDbContext(bool useMasterConnection)
111114
{
112-
if (!useMasterConnection && _isolationOptions == ITestIsolationOptions.SharedTablesAmbientTransaction)
115+
if (!useMasterConnection && _isolationOptions.NeedsAmbientTransaction)
113116
throw new NotSupportedException($"A database transaction cannot be shared among different connections, so the isolation of tests couldn't be guaranteed. Set 'useMasterConnection' to 'true' or 'useSharedTables' to 'false' or use '{nameof(ArrangeDbContext)}/{nameof(ActDbContext)}/{nameof(AssertDbContext)}' which use the same database connection.");
114117

115118
bool isFirstCtx;
@@ -132,7 +135,7 @@ public T CreateDbContext(bool useMasterConnection)
132135
{
133136
RunMigrations(ctx);
134137

135-
if (_isolationOptions == ITestIsolationOptions.SharedTablesAmbientTransaction)
138+
if (_isolationOptions.NeedsAmbientTransaction)
136139
_tx = BeginTransaction(ctx);
137140
}
138141
else if (_tx != null)
@@ -151,27 +154,42 @@ public T CreateDbContext(bool useMasterConnection)
151154
/// <returns>A new instance of the database context.</returns>
152155
protected virtual T CreateDbContext(DbContextOptions<T> options, IDbDefaultSchema schema)
153156
{
154-
var ctx = _contextFactory?.Invoke(options, schema);
157+
var ctx = _contextFactory?.Invoke(options, schema)
158+
?? (_defaultContextFactory ??= CreateDefaultContextFactory())(options, schema);
155159

156-
if (ctx is not null)
157-
return ctx;
160+
return ctx;
161+
}
162+
163+
private static Func<DbContextOptions<T>, IDbDefaultSchema, T> CreateDefaultContextFactory()
164+
{
165+
var optionsType = typeof(DbContextOptions<T>);
166+
var schemaType = typeof(IDbDefaultSchema);
167+
var optionsParam = Expression.Parameter(optionsType);
168+
var schemaParam = Expression.Parameter(schemaType);
169+
Expression[]? ctorArgs = null;
158170

159-
ctx = (T?)Activator.CreateInstance(typeof(T), options, schema);
171+
var ctor = typeof(T).GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, new[] { optionsType, schemaType });
160172

161-
if (ctx is null)
173+
if (ctor is not null)
162174
{
163-
if (_isolationOptions != ITestIsolationOptions.SharedTablesAmbientTransaction)
164-
{
165-
throw new Exception(@$"Could not create an instance of type of '{typeof(T).ShortDisplayName()}' using constructor parameters ({typeof(DbContextOptions<T>).ShortDisplayName()} options, {nameof(IDbDefaultSchema)} schema).
166-
Please provide the corresponding constructor or a custom factory via '{typeof(SqlServerTestDbContextProviderBuilder<T>).ShortDisplayName()}.{nameof(SqlServerTestDbContextProviderBuilder<T>.UseContextFactory)}'.");
167-
}
175+
ctorArgs = new Expression[] { optionsParam, schemaParam };
176+
}
177+
else
178+
{
179+
ctor = typeof(T).GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, new[] { optionsType });
168180

169-
ctx = (T)(Activator.CreateInstance(typeof(T), options)
170-
?? throw new Exception(@$"Could not create an instance of type of '{typeof(T).ShortDisplayName()}' neither using constructor parameters ({typeof(DbContextOptions<T>).ShortDisplayName()} options, {nameof(IDbDefaultSchema)} schema) nor using construct ({typeof(DbContextOptions<T>).ShortDisplayName()} options).
171-
Please provide the corresponding constructor or a custom factory via '{typeof(SqlServerTestDbContextProviderBuilder<T>).ShortDisplayName()}.{nameof(SqlServerTestDbContextProviderBuilder<T>.UseContextFactory)}'."));
181+
if (ctor is not null)
182+
ctorArgs = new Expression[] { optionsParam };
172183
}
173184

174-
return ctx;
185+
if (ctor is null || ctorArgs is null)
186+
{
187+
throw new Exception(@$"Could not create an instance of type of '{typeof(T).ShortDisplayName()}' neither using constructor parameters ({typeof(DbContextOptions<T>).ShortDisplayName()} options, {nameof(IDbDefaultSchema)} schema) nor using construct ({typeof(DbContextOptions<T>).ShortDisplayName()} options).
188+
Please provide the corresponding constructor or a custom factory via '{typeof(SqlServerTestDbContextProviderBuilder<T>).ShortDisplayName()}.{nameof(SqlServerTestDbContextProviderBuilder<T>.UseContextFactory)}'.");
189+
}
190+
191+
return Expression.Lambda<Func<DbContextOptions<T>, IDbDefaultSchema, T>>(Expression.New(ctor, ctorArgs), optionsParam, schemaParam)
192+
.Compile();
175193
}
176194

177195
/// <summary>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ protected virtual string DetermineSchema(bool useSharedTables)
259259
/// <returns>A database schema.</returns>
260260
protected virtual string DetermineSchema(ITestIsolationOptions isolationOptions)
261261
{
262-
return isolationOptions == ITestIsolationOptions.SharedTablesAmbientTransaction
262+
return !isolationOptions.NeedsUniqueSchema
263263
? _sharedTablesSchema ?? "tests"
264264
: Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
265265
}

src/Thinktecture.EntityFrameworkCore.Sqlite.Testing/EntityFrameworkCore/Testing/SqliteTestDbContextProvider.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System.Data.Common;
22
using Microsoft.EntityFrameworkCore.Infrastructure;
33
using Thinktecture.Logging;
4-
using Xunit;
54

65
namespace Thinktecture.EntityFrameworkCore.Testing;
76

tests/Directory.Build.props

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
22

3-
<PropertyGroup>
4-
<ParentPropsFile>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))</ParentPropsFile>
5-
<IsPackable>false</IsPackable>
6-
<NoWarn>$(NoWarn);CA1062</NoWarn>
7-
</PropertyGroup>
3+
<PropertyGroup>
4+
<ParentPropsFile>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))</ParentPropsFile>
5+
<IsPackable>false</IsPackable>
6+
<NoWarn>$(NoWarn);CA1062</NoWarn>
7+
</PropertyGroup>
88

9-
<Import Condition="exists('$(ParentPropsFile)') " Project="$(ParentPropsFile)"/>
9+
<Import Condition="exists('$(ParentPropsFile)') " Project="$(ParentPropsFile)" />
1010

11-
<ItemGroup>
12-
<PackageReference Include="FluentAssertions" Version="6.7.0"/>
13-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0"/>
14-
<PackageReference Include="Moq" Version="4.18.2"/>
15-
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0"/>
16-
<PackageReference Include="Serilog.Sinks.XUnit" Version="3.0.3"/>
17-
<PackageReference Include="xunit" Version="2.4.2"/>
18-
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all"/>
19-
</ItemGroup>
20-
21-
<ItemGroup>
22-
<Using Include="FluentAssertions" />
23-
<Using Include="Moq" />
24-
<Using Include="Xunit" />
25-
<Using Include="Xunit.Abstractions" />
26-
</ItemGroup>
11+
<ItemGroup>
12+
<PackageReference Include="FluentAssertions" Version="6.7.0" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
14+
<PackageReference Include="Moq" Version="4.18.2" />
15+
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
16+
<PackageReference Include="Serilog.Sinks.XUnit" Version="3.0.3" />
17+
<PackageReference Include="xunit" Version="2.4.2" />
18+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<Using Include="FluentAssertions" />
23+
<Using Include="Moq" />
24+
<Using Include="Xunit" />
25+
<Using Include="Xunit.Abstractions" />
26+
</ItemGroup>
2727

2828
</Project>

tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/IntegrationTestsBase.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ public class IntegrationTestsBase : SqlServerDbContextIntegrationTests<TestDbCon
2323
protected Mock<ITenantDatabaseProvider> TenantDatabaseProviderMock { get; }
2424

2525
protected IntegrationTestsBase(ITestOutputHelper testOutputHelper, ITestIsolationOptions isolationOptions)
26-
: base(TestContext.Instance.ConnectionString, isolationOptions, testOutputHelper)
26+
: this(TestContext.Instance.ConnectionString, testOutputHelper, isolationOptions)
27+
{
28+
}
29+
30+
protected IntegrationTestsBase(string connectionString, ITestOutputHelper testOutputHelper, ITestIsolationOptions isolationOptions)
31+
: base(connectionString, isolationOptions, testOutputHelper)
2732
{
2833
TenantDatabaseProviderMock = new Mock<ITenantDatabaseProvider>(MockBehavior.Strict);
2934
}

tests/Thinktecture.EntityFrameworkCore.TestHelpers/Thinktecture.EntityFrameworkCore.TestHelpers.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
<PropertyGroup>
44
<NoWarn>$(NoWarn);CS1591;CA1063;CA1812;CA1816;CA1822;CA2000;EF1001</NoWarn>
5+
<IsTestProject>false</IsTestProject>
56
</PropertyGroup>
67

78
<ItemGroup>
89
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.0" />
910
</ItemGroup>
10-
11+
1112
</Project>
Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3-
<PropertyGroup>
4-
<NoWarn>$(NoWarn);CS1591;CA1063;CA1812;CA1816;CA1822;CA2000;CA2007</NoWarn>
5-
</PropertyGroup>
3+
<PropertyGroup>
4+
<NoWarn>$(NoWarn);CS1591;CA1063;CA1812;CA1816;CA1822;CA2000;CA2007</NoWarn>
5+
<IsTestProject>false</IsTestProject>
6+
</PropertyGroup>
67

7-
<ItemGroup>
8-
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" />
9-
</ItemGroup>
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" />
10+
</ItemGroup>
1011

11-
<ItemGroup>
12+
<ItemGroup>
1213
<ProjectReference Include="..\..\src\Thinktecture.EntityFrameworkCore.Testing\Thinktecture.EntityFrameworkCore.Testing.csproj" />
13-
</ItemGroup>
14+
</ItemGroup>
1415

1516
</Project>

0 commit comments

Comments
 (0)