Skip to content

Commit 7bbf9c4

Browse files
0xcedCopilot
andauthored
Simplify tests by using Testcontainers.Xunit (#92)
* Simplify tests by using Testcontainers.Xunit * EnsureConnectedAsync is not needed anymore, the `UntilDatabaseIsAvailable` wait strategy does exactly this. * Don't blindly swallow exceptions on EnsureCreatedAsync(), only do it for SQL Server where it actually happens and with the precise expected exception. * Use in-memory databases for SQLite so that the file system doesn't get polluted with test databases. * Switch from postgis/postgis to imresamu/postgis:17-3.5 in order to support Apple Silicon Macs. * Use the new `Platform` feature of Testconainers 4.10.0 in order to support vibs2006/sql_server_fts on Apple Silicon Macs. * Rename dbContainer into dbContextFactory, which is more accurate * Remove extra closing parenthesis at the end of the URL Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Rename ServerVersion into MySqlServerVersion * Remove the ATTACH DATABASE command It's not even actually needed for SQLite tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent dd47f26 commit 7bbf9c4

18 files changed

Lines changed: 135 additions & 168 deletions

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/PhenX.EntityFrameworkCore.BulkInsert.Benchmark.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88

99
<ItemGroup>
1010
<PackageReference Include="BenchmarkDotNet" Version="0.15.4" />
11-
<PackageReference Include="Testcontainers.PostgreSql" Version="4.7.0" />
12-
<PackageReference Include="Testcontainers.MsSql" Version="4.7.0" />
13-
<PackageReference Include="Testcontainers.MySql" Version="4.7.0" />
14-
<PackageReference Include="Testcontainers.Oracle" Version="4.7.0" />
11+
<PackageReference Include="Testcontainers.PostgreSql" Version="4.10.0" />
12+
<PackageReference Include="Testcontainers.MsSql" Version="4.10.0" />
13+
<PackageReference Include="Testcontainers.MySql" Version="4.10.0" />
14+
<PackageReference Include="Testcontainers.Oracle" Version="4.10.0" />
1515
</ItemGroup>
1616

1717
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Providers/LibComparatorMySql.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ protected override void ConfigureDbContext()
2222

2323
protected override IDatabaseContainer? GetDbContainer()
2424
{
25-
return new MySqlBuilder()
25+
return new MySqlBuilder("mysql:8.0")
2626
.WithCommand("--log-bin-trust-function-creators=1", "--local-infile=1")
2727
.Build();
2828
}

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Providers/LibComparatorOracle.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ protected override void ConfigureDbContext()
2525

2626
protected override IDatabaseContainer? GetDbContainer()
2727
{
28-
return new OracleBuilder()
29-
.WithImage("gvenzl/oracle-free:23-slim-faststart")
28+
return new OracleBuilder("gvenzl/oracle-free:23-slim-faststart")
3029
.Build();
3130
}
3231
}

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Providers/LibComparatorPostgreSql.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ protected override void ConfigureDbContext()
2525

2626
protected override IDatabaseContainer? GetDbContainer()
2727
{
28-
return new PostgreSqlBuilder()
28+
return new PostgreSqlBuilder("postgres:15.1")
2929
.WithDatabase("testdb")
3030
.WithUsername("testuser")
3131
.WithPassword("testpassword")

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Providers/LibComparatorSqlServer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ protected override void ConfigureDbContext()
2525

2626
protected override IDatabaseContainer? GetDbContainer()
2727
{
28-
return new MsSqlBuilder().Build();
28+
return new MsSqlBuilder("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04").Build();
2929
}
3030
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext;
2+
3+
namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer;
4+
5+
public interface IDbContextFactory
6+
{
7+
Task<TDbContext> CreateContextAsync<TDbContext>(string databaseName)
8+
where TDbContext : TestDbContextBase, new();
9+
}
Lines changed: 25 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,51 @@
11
using System.Data.Common;
2+
using System.Reflection;
23

4+
using DotNet.Testcontainers.Builders;
5+
using DotNet.Testcontainers.Configurations;
36
using DotNet.Testcontainers.Containers;
47

58
using Microsoft.EntityFrameworkCore;
69
using Microsoft.Extensions.Logging.Abstractions;
710

811
using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext;
912

10-
using Xunit;
13+
using Testcontainers.Xunit;
14+
15+
using Xunit.Abstractions;
1116

1217
namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer;
1318

14-
public abstract class TestDbContainer : IAsyncLifetime
19+
public abstract class TestDbContainer<TBuilderEntity, TContainerEntity>(IMessageSink messageSink) : DbContainerFixture<TBuilderEntity, TContainerEntity>(messageSink), IDbContextFactory
20+
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity, IContainerConfiguration>, new()
21+
where TContainerEntity : IContainer, IDatabaseContainer
1522
{
16-
private readonly TimeSpan _waitTime = TimeSpan.FromSeconds(30);
17-
private readonly HashSet<string> _connected = [];
18-
protected readonly IDatabaseContainer? DbContainer;
23+
protected abstract void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName);
24+
25+
protected abstract TBuilderEntity CreateBuilder();
26+
27+
protected virtual string DbmsName => typeof(TContainerEntity).Name.Replace("Container", "");
1928

20-
protected TestDbContainer()
29+
protected override TBuilderEntity Configure()
2130
{
22-
DbContainer = GetDbContainer();
31+
var targetFramework = GetType().Assembly.GetCustomAttributes<AssemblyMetadataAttribute>().FirstOrDefault(e => e.Key == "TargetFramework")?.Value ?? "NA";
32+
return CreateBuilder()
33+
.WithReuse(true)
34+
.WithName($"PhenX.EntityFrameworkCore.BulkInsert.Tests.{DbmsName}-{targetFramework}")
35+
.WithWaitStrategy(Wait.ForUnixContainer().UntilDatabaseIsAvailable(DbProviderFactory));
2336
}
2437

25-
protected abstract IDatabaseContainer? GetDbContainer();
26-
2738
protected virtual string GetConnectionString(string databaseName)
2839
{
29-
if (DbContainer == null)
30-
{
31-
return string.Empty;
32-
}
33-
34-
var builder = new DbConnectionStringBuilder()
35-
{
36-
ConnectionString = DbContainer.GetConnectionString()
37-
};
38-
40+
var builder = DbProviderFactory.CreateConnectionStringBuilder() ?? new DbConnectionStringBuilder();
41+
builder.ConnectionString = ConnectionString;
3942
builder["database"] = databaseName;
4043
return builder.ToString();
4144
}
4245

43-
protected abstract void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName);
44-
45-
public async Task InitializeAsync()
46+
protected virtual async Task EnsureDatabaseCreatedAsync(Microsoft.EntityFrameworkCore.DbContext dbContext)
4647
{
47-
if (DbContainer != null)
48-
{
49-
await DbContainer.StartAsync();
50-
}
48+
await dbContext.Database.EnsureCreatedAsync();
5149
}
5250

5351
public async Task<TDbContext> CreateContextAsync<TDbContext>(string databaseName)
@@ -62,39 +60,8 @@ public async Task<TDbContext> CreateContextAsync<TDbContext>(string databaseName
6260
}
6361
};
6462

65-
if (_connected.Add(databaseName))
66-
{
67-
await EnsureConnectedAsync(dbContext, databaseName);
68-
}
69-
70-
try
71-
{
72-
await dbContext.Database.EnsureCreatedAsync();
73-
}
74-
catch
75-
{
76-
// Often fails with SQL server.
77-
}
63+
await EnsureDatabaseCreatedAsync(dbContext);
7864

7965
return dbContext;
8066
}
81-
82-
protected virtual async Task EnsureConnectedAsync<TDbContext>(TDbContext context, string databaseName)
83-
where TDbContext : TestDbContextBase
84-
{
85-
using var cts = new CancellationTokenSource(_waitTime);
86-
87-
while (!await context.Database.CanConnectAsync(cts.Token))
88-
{
89-
await Task.Delay(100, cts.Token);
90-
}
91-
}
92-
93-
public async Task DisposeAsync()
94-
{
95-
if (DbContainer != null)
96-
{
97-
await DbContainer.DisposeAsync();
98-
}
99-
}
10067
}
Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
using DotNet.Testcontainers.Containers;
1+
using System.Data.Common;
22

33
using Microsoft.EntityFrameworkCore;
44

5+
using MySqlConnector;
6+
57
using PhenX.EntityFrameworkCore.BulkInsert.MySql;
68

9+
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
10+
711
using Testcontainers.MySql;
812

913
using Xunit;
14+
using Xunit.Abstractions;
1015

1116
namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer;
1217

@@ -16,16 +21,22 @@ public class TestDbContainerMySqlCollection : ICollectionFixture<TestDbContainer
1621
public const string Name = "MySql";
1722
}
1823

19-
public class TestDbContainerMySql() : TestDbContainer
24+
public class TestDbContainerMySql(IMessageSink messageSink) : TestDbContainer<MySqlBuilder, MySqlContainer>(messageSink)
2025
{
21-
protected override IDatabaseContainer? GetDbContainer()
26+
private static readonly ServerVersion MySqlServerVersion = ServerVersion.Create(new Version(8, 0), ServerType.MySql);
27+
28+
public override DbProviderFactory DbProviderFactory => MySqlConnectorFactory.Instance;
29+
30+
protected override MySqlBuilder CreateBuilder() => new($"{MySqlServerVersion.TypeIdentifier}:{MySqlServerVersion.Version}");
31+
32+
protected override string DbmsName => MySqlServerVersion.Type.ToString();
33+
34+
protected override MySqlBuilder Configure()
2235
{
23-
return new MySqlBuilder()
36+
return base.Configure()
2437
.WithCommand("--log-bin-trust-function-creators=1", "--local-infile=1", "--innodb-print-all-deadlocks=ON")
25-
.WithReuse(true)
2638
.WithUsername("root")
27-
.WithPassword("root")
28-
.Build();
39+
.WithPassword("root");
2940
}
3041

3142
protected override string GetConnectionString(string databaseName)
@@ -38,18 +49,10 @@ protected override void Configure(DbContextOptionsBuilder optionsBuilder, string
3849
var connectionString = GetConnectionString(databaseName);
3950

4051
optionsBuilder
41-
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), o =>
52+
.UseMySql(connectionString, MySqlServerVersion, o =>
4253
{
4354
o.UseNetTopologySuite();
4455
})
4556
.UseBulkInsertMySql();
4657
}
47-
48-
protected override async Task EnsureConnectedAsync<TDbContext>(TDbContext context, string databaseName)
49-
{
50-
var container = (MySqlContainer)DbContainer!;
51-
52-
await container.ExecScriptAsync($"CREATE DATABASE `{databaseName}`");
53-
await base.EnsureConnectedAsync(context, databaseName);
54-
}
5558
}
Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
using DotNet.Testcontainers.Containers;
1+
using System.Data.Common;
22

33
using Microsoft.EntityFrameworkCore;
44

5+
using Oracle.ManagedDataAccess.Client;
6+
57
using PhenX.EntityFrameworkCore.BulkInsert.Oracle;
68

79
using Testcontainers.Oracle;
810

911
using Xunit;
12+
using Xunit.Abstractions;
1013

1114
namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer;
1215

@@ -16,32 +19,16 @@ public class TestDbContainerOracleCollection : ICollectionFixture<TestDbContaine
1619
public const string Name = "Oracle";
1720
}
1821

19-
public class TestDbContainerOracle : TestDbContainer
22+
public class TestDbContainerOracle(IMessageSink messageSink) : TestDbContainer<OracleBuilder, OracleContainer>(messageSink)
2023
{
21-
protected override IDatabaseContainer? GetDbContainer()
22-
{
23-
return new OracleBuilder()
24-
.WithImage("gvenzl/oracle-free:23-slim-faststart")
25-
.WithReuse(true)
26-
.Build();
27-
}
24+
public override DbProviderFactory DbProviderFactory => OracleClientFactory.Instance;
25+
26+
protected override OracleBuilder CreateBuilder() => new("gvenzl/oracle-free:23-slim-faststart");
2827

2928
protected override void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName)
3029
{
3130
optionsBuilder
32-
.UseOracle(GetConnectionString(databaseName))
31+
.UseOracle(ConnectionString)
3332
.UseBulkInsertOracle();
3433
}
35-
36-
protected override string GetConnectionString(string databaseName)
37-
{
38-
if (DbContainer == null)
39-
{
40-
return string.Empty;
41-
}
42-
43-
var port = DbContainer.GetMappedPublicPort(1521);
44-
45-
return $"Data Source=(DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = {port})) ) (CONNECT_DATA = (SERVICE_NAME = FREEPDB1) ) );User ID=oracle;Password=oracle";
46-
}
4734
}
Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
using DotNet.Testcontainers.Containers;
1+
using System.Data.Common;
22

33
using Microsoft.EntityFrameworkCore;
44

5+
using Npgsql;
6+
57
using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
68

79
using Testcontainers.PostgreSql;
810

911
using Xunit;
12+
using Xunit.Abstractions;
1013

1114
namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer;
1215

@@ -16,18 +19,12 @@ public class TestDbContainerPostgreSqlCollection : ICollectionFixture<TestDbCont
1619
public const string Name = "PostgreSql";
1720
}
1821

19-
public class TestDbContainerPostgreSql : TestDbContainer
22+
public class TestDbContainerPostgreSql(IMessageSink messageSink) : TestDbContainer<PostgreSqlBuilder, PostgreSqlContainer>(messageSink)
2023
{
21-
protected override IDatabaseContainer? GetDbContainer()
22-
{
23-
return new PostgreSqlBuilder()
24-
.WithImage("postgis/postgis") // Geo GeoSpatial support.
25-
.WithReuse(true)
26-
.WithDatabase("testdb")
27-
.WithUsername("testuser")
28-
.WithPassword("testpassword")
29-
.Build();
30-
}
24+
public override DbProviderFactory DbProviderFactory => NpgsqlFactory.Instance;
25+
26+
// GeoSpatial support, using imresamu/postgis instead of postgis/postgis for arm64 support, see https://github.com/postgis/docker-postgis/issues/216#issuecomment-2936824962
27+
protected override PostgreSqlBuilder CreateBuilder() => new("imresamu/postgis:17-3.5");
3128

3229
protected override void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName)
3330
{
@@ -38,12 +35,4 @@ protected override void Configure(DbContextOptionsBuilder optionsBuilder, string
3835
})
3936
.UseBulkInsertPostgreSql();
4037
}
41-
42-
protected override async Task EnsureConnectedAsync<TDbContext>(TDbContext context, string databaseName)
43-
{
44-
var container = (PostgreSqlContainer)DbContainer!;
45-
46-
await container.ExecScriptAsync($"CREATE DATABASE \"{databaseName}\"");
47-
await base.EnsureConnectedAsync(context, databaseName);
48-
}
4938
}

0 commit comments

Comments
 (0)