Skip to content

Commit ab5680e

Browse files
committed
improvement: Moved DB template creation to TestStartup; Removed Extensions.TestContainers again; Simplified test store
1 parent 77c509a commit ab5680e

26 files changed

+237
-653
lines changed

Extensions.slnx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
<Project Path="src/Hangfire/src/Hangfire.csproj" />
2727
<Project Path="src/Hosting/src/Hosting.csproj" />
2828
<Project Path="src/Mediator/src/Mediator.csproj" />
29-
<Project Path="src/TestContainers/src/TestContainers.csproj" />
3029
<Project Path="src/UnitTests.EntityFrameworkCore.Npgsql/src/UnitTests.EntityFrameworkCore.Npgsql.csproj" />
3130
<Project Path="src/UnitTests.EntityFrameworkCore.SqlServer/src/UnitTests.EntityFrameworkCore.SqlServer.csproj" />
3231
<Project Path="src/UnitTests.EntityFrameworkCore/src/UnitTests.EntityFrameworkCore.csproj" />

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,6 @@ Provides simple mediator implementation. See the [documentation](docs/Mediator/R
4242
[![NuGet](https://img.shields.io/nuget/v/Fusonic.Extensions.TestContainers.svg?label=Fusonic.Extensions.TestContainers&style=plastic)](https://www.nuget.org/packages/Fusonic.Extensions.TestContainers/)
4343
Provides simple mediator implementation. See the [documentation](docs/Mediator/README.md) for more details.
4444

45-
[![NuGet](https://img.shields.io/nuget/v/Fusonic.Extensions.UnitTests.svg?label=Fusonic.Extensions.UnitTests&style=plastic)](https://www.nuget.org/packages/Fusonic.Extensions.UnitTests/)
46-
Experimental package that provides a simple TestContainer setup for XUnit v3 test assemblies. See the [test containers documentation](docs/TestContainers/README.md) for more details.
47-
4845
[![NuGet](https://img.shields.io/nuget/v/Fusonic.Extensions.UnitTests.EntityFrameworkCore.svg?label=Fusonic.Extensions.UnitTests.EntityFrameworkCore&style=plastic)](https://www.nuget.org/packages/Fusonic.Extensions.UnitTests.EntityFrameworkCore/)
4946
Adds database support using EF Core to the unit tests. See the [unit test documentation](docs/UnitTests/README.md) for more details.
5047

docs/TestContainers/README.md

Lines changed: 0 additions & 58 deletions
This file was deleted.

docs/UnitTests/README.md

Lines changed: 46 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
- [Test base](#test-base)
99
- [PostgreSQL - Configure DbContext](#postgresql---configure-dbcontext)
1010
- [PostgreSQL - Template](#postgresql---template)
11-
- [PostgreSQL template option: Create it in the fixture](#postgresql-template-option-create-it-in-the-fixture)
12-
- [PostgreSQL template option: Console application](#postgresql-template-option-console-application)
11+
- [PostgreSQL - TestContainers](#postgresql---testcontainers)
1312
- [Microsoft SQL Server - Configure DbContext](#microsoft-sql-server---configure-dbcontext)
13+
- [Microsoft SQL Server - TestContainers](#microsoft-sql-server---testcontainers)
1414
- [Configuring any other database](#configuring-any-other-database)
1515
- [Support mulitple databases in a test](#support-mulitple-databases-in-a-test)
16-
- [Database test concurrency](#database-test-concurrency)
16+
- [Running in GitLab](#running-in-gitlab)
1717

1818
## Introduction
1919

@@ -112,7 +112,9 @@ protected override void RegisterDependencies(Container container)
112112

113113
## Database setup
114114

115-
For database support you can use `Fusonic.Extensions.UnitTests.EntityFrameworkCore`, optionally with specific support for PostgreSQL in `Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql`.
115+
For database support you can use `Fusonic.Extensions.UnitTests.EntityFrameworkCore`, optionally with specific support for
116+
- PostgreSQL in `Fusonic.Extensions.UnitTests.EntityFrameworkCore.Npgsql`
117+
- SQL Server in `Fusonic.Extensions.UnitTests.EntityFrameworkCore.SqlServer`
116118

117119
The basic idea behind those is that every test gets its own database copy. This enables parallel database testing and avoids any issues from tests affecting other tests.
118120

@@ -153,22 +155,15 @@ public class TestFixture : ServiceProviderTestFixture
153155
{
154156
protected sealed override void RegisterCoreDependencies(ServiceCollection services)
155157
{
156-
var options = new NpgsqlDatabasePerTestStoreOptions
157-
{
158-
ConnectionString = Configuration.GetConnectionString("Npgsql")
159-
};
160-
var testStore = new NpgsqlDatabasePerTestStore(options);
158+
var testStore = new NpgsqlDatabasePerTestStore(Configuration.GetConnectionString("Npgsql")); // or TestStartup.ConnectionString when using TestContainers
161159
services.AddSingleton<ITestStore>(testStore);
162160

163161
services.AddDbContext<AppDbContext>(b => b.UseNpgsqlDatabasePerTest(testStore));
164162
}
165163
}
166164
```
167165

168-
The interface of `ITestStore` is straight forward. You can easily replace your test store with something else for another strategy or for supporting other databases.
169-
170166
When using `IDbContextFactory`, the factory must be registered with scoped lifetime, not with the default singleton lifetime.
171-
172167
```cs
173168
services.AddDbContext<AppDbContext>(b => b.UseNpgsqlDatabasePerTest(testStore), ServiceLifetime.Scoped);
174169
```
@@ -177,100 +172,57 @@ services.AddDbContext<AppDbContext>(b => b.UseNpgsqlDatabasePerTest(testStore),
177172

178173
When using the `NpgsqlDatabasePerTest` it is assumed that you use a prepared database template. This template should have all migrations applied and may contain some seeded data. Each test gets a copy of this template. With the `PostgreSqlUtil`, we provide an easy way to create such a template.
179174

180-
You can either create a small console application that creates the template, or do it directly once in the fixture during setup.
181-
182-
#### PostgreSQL template option: Create it in the fixture
175+
You can either create a small console application that creates the template, or do it directly once in the test startup.
183176

184-
You can create the template directly in the TestFixture by specifying a `TemplateCreator` in the options:
177+
A simple way to create that template is provided in `PostgreSqlUtil`. Example:
185178

186179
```cs
187-
protected sealed override void RegisterCoreDependencies(ServiceCollection services)
188-
{
189-
var options = new NpgsqlDatabasePerTestStoreOptions
190-
{
191-
ConnectionString = Configuration.GetConnectionString("Npgsql"),
192-
TemplateCreator = CreateTemplate; // Function to create the template on first connect
193-
};
194-
195-
// rest of the configuration
196-
}
197-
198-
private static Task CreateTemplate(string connectionString)
199-
=> PostgreSqlUtil.CreateTestDbTemplate<AppDbContext>(connectionString, o => new AppDbContext(o), seed: ctx => new TestDataSeed(ctx).Seed());
180+
PostgreSqlUtil.CreateTestDbTemplate<AppDbContext>(templateConnectionString, o => new AppDbContext(o), seed: ctx => new TestDataSeed(ctx).Seed());
200181
```
201182

202-
By default, if the template creator is set, the `TestStore` checks exactly once, if the database exists.
203-
- If the template database exists, no action will be taken. It is not checked, if the database is up to date.
204-
- If the template database does not exist, the `TemplateCreator` is executed.
205-
- All future calls won't do anything and just return.
206-
207-
`PostgreSqlUtil.CreateTestDbTemplate` force drops and recreates your database. However, it won't be called if the datbase already exists.
183+
By default this only creates the template, if the database does not exist yet. This speeds up testing in local development when running tests multiple times. You can change this behaviour to always create a fresh template by setting the parameter `overwrite` to `true`.
208184

209-
In order to get updates to your test database, either drop it or restart your postgresql container, if its data partition is mounted to `tmpfs`.
185+
### PostgreSQL - TestContainers
210186

211-
You can change this behavior to always create a template by setting `options.AlwaysCreateTemplate` to true. In that case, the `TemplateCreator` will always be executed once per test run. This will increase the startup time for your test run though.
187+
To get your test PostgreSQL server up and running, a simple solution is to use [TestContainers](https://testcontainers.com/) and start one during test startup. This ensures that the test configuration in your local development and in your CI pipelines is the same.
212188

213-
#### PostgreSQL template option: Console application
214-
Alternatively, if you prefer to create the test database externally before the test run, create a console application with the following code in `Program.cs`:
215-
216-
```cs
217-
if (args.Length == 0)
218-
{
219-
Console.Out.WriteLine("Missing connection string.");
220-
return 1;
221-
}
222-
223-
PostgreSqlUtil.CreateTestDbTemplate<AppDbContext>(args[0], o => new AppDbContext(o), seed: ctx => new TestDataSeed(ctx).Seed());
224-
225-
return 0;
226-
```
227-
228-
With that, the database given in the connection string is getting force dropped, recreated, migrations applied and optionally seeded via the given `TestDataSeed`. You can simply call it in your console or the build pipeline before running the tests using
229-
```sh
230-
dotnet run --project <pathToCsProject> "<connectionString>"
231-
```
189+
For an example how we use TestContainers see [UnitTests.EntityFrameworkCore.Npgsql.Tests.TestStartup](../../src/UnitTests.EntityFrameworkCore.Npgsql/test/TestStartup.cs) and [TestFixture](../../src/UnitTests.EntityFrameworkCore.Npgsql/test/TestFixture.cs)
232190

233191
### Microsoft SQL Server - Configure DbContext
234192

235-
A `TestStore` is used for handling the test databases. For Microsoft SQL Server, you can use the `SqlServerDatabasePerTestStore`, which creates a separate database for each test. You have to pass the connection string to the database and a method to create the test database. Register it as follows:
193+
A `TestStore` is used for handling the test databases. For Microsoft SQL Server, you can use the `SqlServerDatabasePerTestStore`, which creates a separate database for each test. You just have to pass it the connection string to the template and register it as follows:
236194

237195
```cs
238196
public class TestFixture : ServiceProviderTestFixture
239197
{
240198
protected sealed override void RegisterCoreDependencies(ServiceCollection services)
241199
{
242-
var options = new SqlServerDatabasePerTestStoreOptions
243-
{
244-
ConnectionString = Configuration.GetConnectionString("SqlServer")!,
245-
TemplateCreator = CreateSqlServerTemplate,
246-
DatabasePrefix = "project_test_" // Optional. Defines a prefix for the randomly generated test database names.
247-
DatabaseDirectoryPath = "C:/mssql/data" // Optional. Defaults to docker image default path.
248-
};
249-
var testStore = new SqlServerDatabasePerTestStore(options);
200+
var testStore = new SqlServerDatabasePerTestStore(Configuration.GetConnectionString("SqlServer")); // or TestStartup.ConnectionString when using TestContainers
250201
services.AddSingleton<ITestStore>(testStore);
202+
251203
services.AddDbContext<AppDbContext>(b => b.UseSqlServerDatabasePerTest(testStore));
252204
}
253-
254-
private static async Task CreateSqlServerTemplate(string connectionString)
255-
=> await SqlServerTestUtil.CreateTestDbTemplate<SqlServerDbContext>(connectionString, o => new SqlServerDbContext(o));
256205
}
257206
```
258207

259208
The connection string must have the `Intial catalog` set. It determines the name of the template database. All tests will use a copy of the template database.
260209

261-
The `TemplateCreator` specifies the method to create a template. It has to create and seed the database and create a backup for the copies used for the tests. Fortunately, the `SqlServerTestUtil` provides a method to do exactly that.
262-
263210
When using `IDbContextFactory`, the factory must be registered with scoped lifetime, not with the default singleton lifetime.
264-
265211
```cs
266212
services.AddDbContext<AppDbContext>(b => b.UseSqlServerDatabasePerTest(testStore), ServiceLifetime.Scoped);
267213
```
268214

215+
### Microsoft SQL Server - TestContainers
216+
217+
To get your test SQL Server server up and running, a simple solution is to use [TestContainers](https://testcontainers.com/) and start one during test startup. This ensures that the test configuration in your local development and in your CI pipelines is the same.
218+
219+
For an example how we use TestContainers see [UnitTests.EntityFrameworkCore.SqlServer.Tests.TestStartup](../../src/UnitTests.EntityFrameworkCore.SqlServer/test/TestStartup.cs) and [TestFixture](../../src/UnitTests.EntityFrameworkCore.SqlServer/test/TestFixture.cs)
220+
269221
### Configuring any other database
270222

271223
The database support is not limited to PostgreSql and SQL Server. You just have to implement and register the `ITestStore`.
272224

273-
For a simple example with SqLite, check `Fusonic.Extensions.UnitTests.EntityFrameworkCore.Tests` -> `SqliteTestStore` and `TestFixture`.
225+
For a simple example with SqLite, check [UnitTests.EntityFrameworkCore.Tests.SqliteTestStore](../../src/UnitTests.EntityFrameworkCore/test/SqliteTestStore.cs) and [TestFixture](../../src/UnitTests.EntityFrameworkCore/test/TestFixture.cs).
274226

275227
### Support mulitple databases in a test
276228

@@ -281,74 +233,38 @@ public class TestFixture : ServiceProviderTestFixture
281233
{
282234
private void RegisterDatabase(IServiceCollection services)
283235
{
284-
// Register Npgsql (PostgreSQL)
285-
var npgsqlSettings = new NpgsqlDatabasePerTestStoreOptions
286-
{
287-
ConnectionString = Configuration.GetConnectionString("Npgsql"),
288-
TemplateCreator = CreatePostgresTemplate
289-
};
290-
291-
var npgsqlTestStore = new NpgsqlDatabasePerTestStore(npgsqlSettings);
236+
// Register Npgsql (PostgreSQL) using TestContainers in the TestStartup
237+
var npgsqlTestStore = new NpgsqlDatabasePerTestStore(TestStartup.NpgsqlConnectionString);
292238
services.AddDbContext<NpgsqlDbContext>(b => b.UseNpgsqlDatabasePerTest(npgsqlTestStore));
293239

294-
// Register SQL Server
295-
var sqlServerSettings = new SqlServerDatabasePerTestStoreOptions
296-
{
297-
ConnectionString = Configuration.GetConnectionString("SqlServer")!,
298-
TemplateCreator = CreateSqlServerTemplate
299-
};
300-
var sqlServerTestStore = new SqlServerDatabasePerTestStore(sqlServerSettings);
240+
// Register SQL Server using TestContainers in the TestStartup
241+
var sqlServerTestStore = new SqlServerDatabasePerTestStore(TestStartup.SqlServerConnectionString);
301242
services.AddDbContext<SqlServerDbContext>(b => b.UseSqlServerDatabasePerTest(sqlServerTestStore));
302243

303244
// Combine the test stores in the AggregateTestStore
304245
services.AddSingleton<ITestStore>(new AggregateTestStore(npgsqlTestStore, sqlServerTestStore));
305246
}
306-
307-
private static Task CreatePostgresTemplate(string connectionString)
308-
=> PostgreSqlUtil.CreateTestDbTemplate<NpgsqlDbContext>(connectionString, o => new NpgsqlDbContext(o), seed: ctx => new TestDataSeed(ctx).Seed());
309-
310-
private static Task CreateSqlServerTemplate(string connectionString)
311-
=> SqlServerTestUtil.CreateTestDbTemplate<SqlServerDbContext>(connectionString, o => new SqlServerDbContext(o));
312247
}
313248
```
314249

315-
### Database test concurrency
316-
317-
XUnit limits the number the maximum _active_ tests executing, but it does not the limit of maximum parallel tests.
318-
Simplified, as soon as a test awaits a task somewhere, the thread is returned to the pool and another test gets started. This is intended by design.
250+
## Running in GitLab
319251

320-
This behavior can cause issues when running integration tests against a database, especially when lots of tests are started. Connection limits can be exhausted quickly.
252+
In order to use TestContainers in GitLab, start `docker:dind-rootless` as a service. When running rootless dind, you also must set `TESTCONTAINERS_RYUK_DISABLED` to `true`, as there is no `docker.sock` available. Ryuk is responsible for cleaning up the test containers, even when test jobs get cancelled. Disabling it should be safe though, as the containers started within `docker:dind` get cleaned up anyway after the job ends.
321253

322-
To solve this, you can either throttle your tests, or increase the max. connections of your test database.
323-
324-
To increase the max. connections of your postgres test instance, just pass the parameter max_connections. Example for a docker compose file:
254+
Example:
325255
```yaml
326-
postgres_test:
327-
image: postgres:17
328-
command: -c max_connections=500
329-
ports:
330-
- "5433:5432"
331-
volumes:
332-
- type: tmpfs
333-
target: /var/lib/postgresql/data
334-
- type: tmpfs
335-
target: /dev/shm
336-
environment:
337-
POSTGRES_PASSWORD: developer
338-
```
339-
340-
Alternatively, if you want to throttle your tests instead, you can to this easily with a semaphore in your test base:
341-
342-
```cs
343-
public class TestBase : IAsyncDisposable
344-
{
345-
private static readonly SemaphoreSlim Throttle = new(64);
346-
public async Task InitializeAsync() => await Throttle.WaitAsync();
347-
348-
public ValueTask DisposeAsync()
349-
{
350-
_ = Throttle.Release();
351-
return ValueTask.CompletedTask;
352-
}
353-
}
354-
```
256+
variables:
257+
DOCKER_VERSION: "29"
258+
DOCKER_BUILDKIT: 1
259+
DOCKER_HOST: tcp://docker:2376
260+
DOCKER_TLS_CERTDIR: "/certs/${CI_JOB_ID}"
261+
DOCKER_TLS_VERIFY: 1
262+
DOCKER_CERT_PATH: "/certs/${CI_JOB_ID}/client"
263+
TESTCONTAINERS_RYUK_DISABLED: "true" # Disable Ryuk as we're running in dind-rootless and there is no docker.sock available.
264+
265+
dotnet:test:
266+
services:
267+
- docker:${DOCKER_VERSION}-dind-rootless
268+
script:
269+
- echo "Running Tests..."
270+
```

src/Hangfire/test/Hangfire.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<PackageReference Include="Microsoft.Testing.Extensions.TrxReport" />
2121
<PackageReference Include="NSubstitute" />
2222
<PackageReference Include="SimpleInjector.Integration.ServiceCollection" />
23+
<PackageReference Include="Testcontainers.PostgreSql" />
2324
<PackageReference Include="xunit.v3.core" />
2425
<PackageReference Include="xunit.analyzers">
2526
<PrivateAssets>all</PrivateAssets>
@@ -33,7 +34,6 @@
3334
</ItemGroup>
3435

3536
<ItemGroup>
36-
<ProjectReference Include="..\..\TestContainers\src\TestContainers.csproj" />
3737
<ProjectReference Include="..\..\UnitTests.EntityFrameworkCore.Npgsql\src\UnitTests.EntityFrameworkCore.Npgsql.csproj" />
3838
<ProjectReference Include="..\..\UnitTests.EntityFrameworkCore\src\UnitTests.EntityFrameworkCore.csproj" />
3939
<ProjectReference Include="..\..\UnitTests\src\UnitTests.csproj" />

0 commit comments

Comments
 (0)