Skip to content
PaulusParssinenPublic

About

.NET library for fast database-backed tests using PostgreSQL template databases.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Tempgres NuGet

Tempgres creates isolated PostgreSQL databases for integration tests by cloning a migrated template database.

It is useful when your tests need real PostgreSQL behavior, but you do not want to start a new PostgreSQL container for every test case or test class.

Tempgres is heavily inspired by pgtestdb, which applies the same PostgreSQL template-database idea in Go.

Note

pgtestdb supports creating and using a dedicated role for template and test databases. Tempgres does not include role management today but it can be added if theres ask for it.

Requirements

Tempgres needs an administrative connection string to a disposable PostgreSQL instance.

Do not point Tempgres at a shared development, staging, or production server.

For local development, an in-memory container is usually enough (tune the tmpfs-size etc. for your needs):

docker run -d --name tempgres-test --rm \
  -e POSTGRES_PASSWORD=postgres \
  -p 15432:5432 \
  --mount type=tmpfs,destination=/var/lib/postgresql,tmpfs-size=1g \
  postgres:18 \
  postgres \
  -c full_page_writes=off \
  -c shared_buffers=64MB \
  -c client_min_messages=warning

Use this as the provisioning connection string, either directly as TempgresOptions.AdminConnectionString or through TEMPGRES_CONNECTION_STRING in your tests:

Host=127.0.0.1;Port=15432;Username=postgres;Password=postgres;Include Error Detail=true

Your application tests should use the acquired TempgresDatabase.ConnectionString.

Usage

Notes

  • Use a dedicated disposable PostgreSQL server for tests.
  • Dispose acquired TempgresDatabase instances with await using whenever possible.
  • Keep application connection pooling disabled for acquired database connection strings. Tempgres does this by default for returned connection strings.

Timeouts

TempgresOptions.LockAcquisitionTimeout controls the advisory lock wait (default 30 seconds). TemplateConnectionCloseTimeout controls how long Tempgres waits for sessions on a prepared template to exit (default 10 seconds). CloneRetryTimeout controls how long it retries a clone blocked by a template session (default 30 seconds); set it to TimeSpan.Zero to disable retries. For example:

EF Core

var factory = new TempgresDatabaseFactory(new TempgresOptions
{
    AdminConnectionString = "...",
    TemplateDatabaseName = "myapp_tests",
    Migrator = new EfCoreMigrator<AppDbContext>()
});

await using var database = await factory.AcquireAsync(cancellationToken);

var builder = new DbContextOptionsBuilder<AppDbContext>()
    .UseNpgsql(database.ConnectionString);

await using var db = new AppDbContext(builder.Options);

The first acquisition prepares the template database by running the migrator. Later acquisitions clone that template and return unique database names. Disposing the TempgresDatabase drops the test database.

EF Core + ASP.NET Core with WebApplicationFactory

If your application reads a named connection string when configuring EF Core, reference Tempgres.AspNetCore.Testing and Tempgres.EntityFrameworkCore from the test project. The testing integration supplies the connection string and owns the whole HTTP test environment:

using Tempgres;
using Tempgres.AspNetCore.Testing;
using Tempgres.EntityFrameworkCore;

// In Program.cs: options.UseNpgsql(builder.Configuration.GetConnectionString("Default"));

var factory = new TempgresDatabaseFactory(new TempgresOptions
{
    AdminConnectionString = Environment.GetEnvironmentVariable("TEMPGRES_CONNECTION_STRING")
        ?? throw new InvalidOperationException("Set TEMPGRES_CONNECTION_STRING to a disposable PostgreSQL server."),
    TemplateDatabaseName = "myapp_api_tests",
    Migrator = new EfCoreMigrator<AppDbContext>()
});

await using var app = await factory.AcquireWebApplicationFactoryLeaseAsync<Program>("Default");

using var response = await app.Client.GetAsync("/api/todos");

The lease exposes Client, Factory, and Database. It disposes the HTTP client and web application factory before dropping the cloned database. See example use with a fixture: IntegrationFixture.cs and OrderCheckoutTests.cs.

For applications that register a DbContext or data source without using a named connection string, pass a callback that creates a configured WebApplicationFactory<Program> instead. The lease still handles host and database cleanup.

Dependency Injection Without ASP.NET Core

For applications that use Microsoft.Extensions.DependencyInjection, acquire the database first and build the service provider from that database:

await using var app = await factory.AcquireServiceProviderLeaseAsync(database =>
{
    var services = new ServiceCollection();

    services.AddMyApplicationServices();
    services.AddDbContext<AppDbContext>(options => options.UseNpgsql(database.ConnectionString));

    return services.BuildServiceProvider(validateScopes: true);
}, cancellationToken);

await using var scope = app.Services.CreateAsyncScope();
var command = scope.ServiceProvider.GetRequiredService<ImportUsersCommand>();
await command.RunAsync(cancellationToken);

The service provider lease owns the temporary database above normal DI scopes. Disposing the lease disposes the service provider first, then drops the database.

EF Core + Seeded Template Data

Reference data can be seeded once into the template and cloned for every test database.

var options = new TempgresOptions
{
    AdminConnectionString = "...",
    TemplateDatabaseName = "myapp_seeded"
};

options.UseEfCoreMigrations<AppDbContext>(
    contextFactory: dbOptions => new AppDbContext(dbOptions),
    seedAsync: async (db, cancellationToken) =>
    {
        db.Roles.Add(new Role { Name = "Admin" });
        db.OrderStatuses.Add(new OrderStatus { Code = "submitted" });
        await db.SaveChangesAsync(cancellationToken);
    });

Conceptual Model

  1. Tempgres resolves a template name from TemplateDatabaseName plus the migrator hash.
  2. It takes a PostgreSQL advisory lock so concurrent test workers do not create the same template twice.
  3. It creates the template database and runs migrations/seeding once.
  4. It closes the template to new connections, ends any remaining sessions, and marks the template as complete.
  5. Each test calls AcquireAsync, which creates a unique database with CREATE DATABASE ... WITH TEMPLATE ....
  6. Disposing the acquired database drops only that test database.

The template itself cannot be connected to after preparation. Existing templates made by older Tempgres versions are closed to connections when a new factory first uses them.

Debugging Failed Tests

Tempgres drops a test database when its TempgresDatabase is disposed.

When debugging a failure, you can temporarily avoid disposing the acquired database and connect to TempgresDatabase.ConnectionString. The database name is also available as TempgresDatabase.DatabaseName.

CI Setup

In GitHub Actions, run PostgreSQL as a service or start the Docker container before dotnet test. The key is to expose a stable host/port and pass the admin connection string to the tests.

For example, for GitHub Actions workflows:

services:
  postgres:
    image: postgres:18-alpine
    env:
      POSTGRES_PASSWORD: postgres
    ports:
      - 15432:5432
    options: >-
      --tmpfs /var/lib/postgresql:rw,size=1g
      --health-cmd "pg_isready -U postgres"
      --health-interval 5s
      --health-timeout 3s
      --health-retries 5

env:
  TEMPGRES_CONNECTION_STRING: Host=127.0.0.1;Port=15432;Username=postgres;Password=postgres;Include Error Detail=true

About

.NET library for fast database-backed tests using PostgreSQL template databases.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages