Skip to content

Commit f550203

Browse files
authored
Merge pull request #163 from 0xced/Testcontainers-Fixture
Use fixtures to speed up database tests
2 parents f87da03 + b1f6985 commit f550203

19 files changed

+1249
-1199
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ jobs:
2828
- name: Build and Test
2929
run: ./Build.ps1
3030
shell: pwsh
31+
- name: Test Report
32+
uses: dorny/test-reporter@v2
33+
if: always()
34+
with:
35+
name: Test Report
36+
path: artifacts/*.trx
37+
reporter: dotnet-trx
3138
- name: Push to MyGet
3239
env:
3340
NUGET_URL: https://www.myget.org/F/respawn-ci/api/v3/index.json

Build.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@ exec { & dotnet clean --configuration Release }
3030

3131
exec { & dotnet build --configuration Release }
3232

33-
exec { & dotnet test --configuration Release --results-directory $artifacts --no-build --logger trx --verbosity=normal }
33+
exec { & dotnet test --configuration Release --results-directory $artifacts --no-build --verbosity=normal }
3434

3535
exec { & dotnet pack .\Respawn\Respawn.csproj --configuration Release --output $artifacts --no-build }

Directory.Build.targets

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<Project>
2+
<PropertyGroup>
3+
<VSTestLogger>trx%3BLogFileName=$(MSBuildProjectName)-$(TargetFramework).trx</VSTestLogger>
4+
</PropertyGroup>
5+
</Project>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
using System.Data.Common;
3+
using DotNet.Testcontainers.Images;
4+
using IBM.Data.Db2;
5+
using NPoco;
6+
using Testcontainers.Db2;
7+
using Xunit.Abstractions;
8+
9+
namespace Respawn.DatabaseTests;
10+
11+
public class DB2Fixture(IMessageSink messageSink) : DbFixture<Db2Builder, Db2Container>(messageSink)
12+
{
13+
public override DbProviderFactory DbProviderFactory => DB2Factory.Instance;
14+
15+
protected override DatabaseType DbType => null;
16+
17+
protected override Db2Builder CreateBuilder() => new Db2Builder(new DockerImage("icr.io/db2_community/db2:12.1.0.0", new Platform("amd64"))).WithAcceptLicenseAgreement(true);
18+
}

Respawn.DatabaseTests/DB2Tests.cs

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,21 @@
11
using Shouldly;
22
using System;
33
using System.Threading.Tasks;
4-
using DotNet.Testcontainers.Builders;
5-
using DotNet.Testcontainers.Images;
64
using IBM.Data.Db2;
75
using NPoco;
86
using Respawn.Graph;
9-
using Testcontainers.Db2;
107
using Xunit;
118
using Xunit.Abstractions;
129

1310
namespace Respawn.DatabaseTests
1411
{
15-
public class DB2Tests : IAsyncLifetime
12+
public class DB2Tests(ITestOutputHelper output, DB2Fixture fixture) : IAsyncLifetime, IClassFixture<DB2Fixture>
1613
{
17-
private Db2Container _sqlContainer;
1814
private DB2Connection _connection;
19-
private readonly ITestOutputHelper _output;
2015

21-
public DB2Tests(ITestOutputHelper output)
22-
{
23-
_output = output;
24-
}
25-
26-
public async Task InitializeAsync()
27-
{
28-
_sqlContainer = new Db2Builder()
29-
.WithAcceptLicenseAgreement(true)
30-
.Build();
31-
await _sqlContainer.StartAsync();
32-
33-
_connection = new DB2Connection(_sqlContainer.GetConnectionString());
34-
35-
await _connection.OpenAsync();
36-
}
37-
38-
public async Task DisposeAsync()
39-
{
40-
_connection?.Close();
41-
_connection?.Dispose();
42-
_connection = null;
16+
public async Task InitializeAsync() => _connection = (DB2Connection)await fixture.OpenConnectionAsync();
4317

44-
await _sqlContainer.StopAsync();
45-
await _sqlContainer.DisposeAsync();
46-
_sqlContainer = null;
47-
}
18+
public async Task DisposeAsync() => await _connection.DisposeAsync();
4819

4920
[SkipOnCI]
5021
public async Task ShouldDeleteData()
@@ -151,7 +122,7 @@ FOREIGN KEY (FooValue) REFERENCES Foo(Value)
151122
}
152123
catch
153124
{
154-
_output.WriteLine(checkPoint.DeleteSql ?? string.Empty);
125+
output.WriteLine(checkPoint.DeleteSql ?? string.Empty);
155126
throw;
156127
}
157128

@@ -221,7 +192,7 @@ ParentId INT NULL
221192
}
222193
catch
223194
{
224-
_output.WriteLine(checkPoint.DeleteSql ?? string.Empty);
195+
output.WriteLine(checkPoint.DeleteSql ?? string.Empty);
225196
throw;
226197
}
227198

@@ -275,7 +246,7 @@ ParentId INT NULL
275246
}
276247
catch
277248
{
278-
_output.WriteLine(checkPoint.DeleteSql ?? string.Empty);
249+
output.WriteLine(checkPoint.DeleteSql ?? string.Empty);
279250
throw;
280251
}
281252

@@ -359,7 +330,7 @@ public async Task ShouldHandleComplexCycles()
359330
}
360331
catch
361332
{
362-
_output.WriteLine(checkPoint.DeleteSql ?? string.Empty);
333+
output.WriteLine(checkPoint.DeleteSql ?? string.Empty);
363334
throw;
364335
}
365336

@@ -429,7 +400,7 @@ public async Task ShouldExcludeSchemas()
429400
}
430401
catch
431402
{
432-
_output.WriteLine(checkPoint.DeleteSql ?? string.Empty);
403+
output.WriteLine(checkPoint.DeleteSql ?? string.Empty);
433404
throw;
434405
}
435406

@@ -489,7 +460,7 @@ public async Task ShouldIncludeSchemas()
489460
}
490461
catch
491462
{
492-
_output.WriteLine(checkPoint.DeleteSql ?? string.Empty);
463+
output.WriteLine(checkPoint.DeleteSql ?? string.Empty);
493464
throw;
494465
}
495466

Respawn.DatabaseTests/DbFixture.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Runtime.CompilerServices;
3+
using System.Threading.Tasks;
4+
using DotNet.Testcontainers.Builders;
5+
using DotNet.Testcontainers.Configurations;
6+
using DotNet.Testcontainers.Containers;
7+
using NPoco;
8+
using Testcontainers.Xunit;
9+
using Xunit.Abstractions;
10+
11+
namespace Respawn.DatabaseTests;
12+
13+
public abstract class DbFixture<TBuilderEntity, TContainerEntity>(IMessageSink messageSink) : DbContainerFixture<TBuilderEntity, TContainerEntity>(messageSink)
14+
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity, IContainerConfiguration>, new()
15+
where TContainerEntity : IDatabaseContainer
16+
{
17+
public async Task<IDatabase> CreateDatabaseAsync([CallerMemberName] string dbName = "")
18+
{
19+
await ExecuteCreateDatabaseAsync(dbName);
20+
21+
var connectionString = GetConnectionString(dbName);
22+
var database = new Database(connectionString, DbType, DbProviderFactory);
23+
return database.OpenSharedConnection();
24+
}
25+
26+
protected abstract DatabaseType DbType { get; }
27+
28+
protected abstract TBuilderEntity CreateBuilder();
29+
30+
protected override TBuilderEntity Configure() => CreateBuilder().WithWaitStrategy(Wait.ForUnixContainer().UntilDatabaseIsAvailable(DbProviderFactory));
31+
32+
protected virtual async Task ExecuteCreateDatabaseAsync(string dbName)
33+
{
34+
var builder = DbProviderFactory.CreateCommandBuilder();
35+
var sql = $"CREATE DATABASE {builder?.QuotePrefix}{dbName}{builder?.QuoteSuffix}";
36+
37+
await using var command = CreateCommand(sql);
38+
await command.ExecuteNonQueryAsync();
39+
}
40+
41+
protected virtual string GetConnectionString(string dbName)
42+
{
43+
var builder = DbProviderFactory.CreateConnectionStringBuilder() ?? throw new InvalidOperationException($"CreateConnectionStringBuilder returned null for {DbProviderFactory}");
44+
builder.ConnectionString = ConnectionString;
45+
builder["Database"] = dbName;
46+
return builder.ToString();
47+
}
48+
}

Respawn.DatabaseTests/EmptyDbTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public EmptyDbTests(ITestOutputHelper output)
2323

2424
public async Task InitializeAsync()
2525
{
26-
_msSqlContainer = new MsSqlBuilder().Build();
26+
_msSqlContainer = new MsSqlBuilder("mcr.microsoft.com/mssql/server:2022-CU23-ubuntu-22.04").Build();
2727
await _msSqlContainer.StartAsync();
2828

2929
var connString = _msSqlContainer.GetConnectionString();
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
using System;
2+
using System.Data.Common;
3+
using System.IO;
4+
using Docker.DotNet.Models;
5+
using DotNet.Testcontainers.Builders;
6+
using DotNet.Testcontainers.Configurations;
7+
using DotNet.Testcontainers.Containers;
8+
using DotNet.Testcontainers.Images;
9+
using IBM.Data.Db2;
10+
using NPoco;
11+
using Xunit.Abstractions;
12+
13+
namespace Respawn.DatabaseTests;
14+
15+
public class InformixFixture(IMessageSink messageSink) : DbFixture<InformixBuilder, InformixContainer>(messageSink)
16+
{
17+
public override DbProviderFactory DbProviderFactory => DB2Factory.Instance;
18+
19+
protected override DatabaseType DbType => null;
20+
21+
protected override InformixBuilder CreateBuilder()
22+
{
23+
return new InformixBuilder("ibmcom/informix-developer-database:14.10.FC5DE");
24+
}
25+
}
26+
27+
public sealed class InformixBuilder : ContainerBuilder<InformixBuilder, InformixContainer, ContainerConfiguration>
28+
{
29+
public InformixBuilder()
30+
: this("")
31+
{
32+
throw new NotSupportedException();
33+
}
34+
35+
public InformixBuilder(string image)
36+
: this(new DockerImage(image))
37+
{
38+
}
39+
40+
public InformixBuilder(IImage image)
41+
: this(new ContainerConfiguration())
42+
{
43+
DockerResourceConfiguration = Init().WithImage(image).DockerResourceConfiguration;
44+
}
45+
46+
private InformixBuilder(ContainerConfiguration configuration) : base(configuration)
47+
{
48+
DockerResourceConfiguration = configuration;
49+
}
50+
51+
protected override ContainerConfiguration DockerResourceConfiguration { get; }
52+
53+
protected override InformixBuilder Init()
54+
{
55+
return base.Init()
56+
57+
// = environment:
58+
.WithEnvironment("LICENSE", "accept")
59+
.WithEnvironment("ONCONFIG_FILE", "onconfig")
60+
.WithEnvironment("RUN_FILE_PRE_INIT", "my_post.sh")
61+
62+
// = ports:
63+
.WithPortBinding(9088, assignRandomHostPort: true)
64+
.WithPortBinding(9089, assignRandomHostPort: true)
65+
.WithPortBinding(27017, assignRandomHostPort: true)
66+
.WithPortBinding(27018, assignRandomHostPort: true)
67+
.WithPortBinding(27883, assignRandomHostPort: true)
68+
69+
// = volumes:
70+
.WithBindMount(
71+
source: Path.GetFullPath("./informix-server"),
72+
destination: "/opt/ibm/config",
73+
AccessMode.ReadWrite)
74+
75+
// = privileged: true
76+
.WithPrivileged(true)
77+
78+
// = user: root
79+
//.WithUser("root")
80+
81+
// = tty: true
82+
//.WithTty(true)
83+
84+
// optional: equivalent to "restart: always" but Testcontainers
85+
// does not automatically restart containers (it recreates instead)
86+
// .WithAutoRemove(false)
87+
88+
.WithWaitStrategy(Wait.ForUnixContainer()
89+
.UntilExternalTcpPortIsAvailable(9088)
90+
.UntilExternalTcpPortIsAvailable(9089)
91+
.UntilInternalTcpPortIsAvailable(9088)
92+
.UntilInternalTcpPortIsAvailable(9089)
93+
// This is the last success message
94+
.UntilMessageIsLogged("starting mqtt listener on port 27883")
95+
);
96+
}
97+
98+
public override InformixContainer Build()
99+
{
100+
Validate();
101+
return new InformixContainer(DockerResourceConfiguration);
102+
}
103+
104+
protected override InformixBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
105+
{
106+
return Merge(DockerResourceConfiguration, new ContainerConfiguration(resourceConfiguration));
107+
}
108+
109+
protected override InformixBuilder Clone(IContainerConfiguration resourceConfiguration)
110+
{
111+
return Merge(DockerResourceConfiguration, new ContainerConfiguration(resourceConfiguration));
112+
}
113+
114+
protected override InformixBuilder Merge(ContainerConfiguration oldValue, ContainerConfiguration newValue)
115+
{
116+
return new InformixBuilder(new ContainerConfiguration(oldValue, newValue));
117+
}
118+
}
119+
120+
public sealed class InformixContainer(IContainerConfiguration configuration) : DockerContainer(configuration), IDatabaseContainer
121+
{
122+
public string GetConnectionString()
123+
{
124+
var host = Hostname;
125+
var port = GetMappedPublicPort(9089); // SQL port
126+
return $"Server={host}:{port};Database=sysadmin;UID=informix;Password=in4mix;Persist Security Info=True;Authentication=Server;";
127+
}
128+
}

0 commit comments

Comments
 (0)