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.
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=warningUse 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.
- Use a dedicated disposable PostgreSQL server for tests.
- Dispose acquired
TempgresDatabaseinstances withawait usingwhenever possible. - Keep application connection pooling disabled for acquired database connection strings. Tempgres does this by default for returned connection strings.
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:
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.
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.
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.
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);
});- Tempgres resolves a template name from
TemplateDatabaseNameplus the migrator hash. - It takes a PostgreSQL advisory lock so concurrent test workers do not create the same template twice.
- It creates the template database and runs migrations/seeding once.
- It closes the template to new connections, ends any remaining sessions, and marks the template as complete.
- Each test calls
AcquireAsync, which creates a unique database withCREATE DATABASE ... WITH TEMPLATE .... - 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.
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.
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