DRN.Framework.Testing
0.9.8
Prefix Reserved
See the version list below for details.
dotnet add package DRN.Framework.Testing --version 0.9.8
NuGet\Install-Package DRN.Framework.Testing -Version 0.9.8
<PackageReference Include="DRN.Framework.Testing" Version="0.9.8" />
<PackageVersion Include="DRN.Framework.Testing" Version="0.9.8" />
<PackageReference Include="DRN.Framework.Testing" />
paket add DRN.Framework.Testing --version 0.9.8
#r "nuget: DRN.Framework.Testing, 0.9.8"
#:package DRN.Framework.Testing@0.9.8
#addin nuget:?package=DRN.Framework.Testing&version=0.9.8
#tool nuget:?package=DRN.Framework.Testing&version=0.9.8
DRN.Framework.Testing
Practical, effective testing helpers with data attributes, test context, and container orchestration for unit and integration tests.
TL;DR
- Auto-Mocking -
[DataInline]/[DataInlineUnit]provide requested context objects and auto-mock interface parameters with NSubstitute - Container Context - Postgres migration binding on demand; RabbitMQ is available as an explicit opt-in container helper
- Application Context -
WebApplicationFactoryintegration that syncs services/configuration and binds Postgres dependencies before client creation - Convention-Based - Settings and data files auto-discovered from test folder hierarchy
- DTT Pattern - Integration-first tests with minimal setup, AwesomeAssertions, and MTP-friendly execution
Table of Contents
- QuickStart: Beginner
- QuickStart: Advanced
- DrnTestContext
- ContainerContext
- ApplicationContext
- Local Development Experience
- Data Attributes
- Unit Testing
- DebugOnly Tests
- DI Health Validation
- JSON Utilities
- FlurlHttpTest Integration
- Providers
- Example Test Project
- Test Snippet
- Testing Guide and DTT Approach
- Global Usings
- Related Packages
QuickStart: Beginner
For parameterless unit tests without context or generated parameters, standard [Fact] is preferred.
Write auto-mocked tests in seconds using [DataInlineUnit] (for unit tests) or [DataInline] (for integration tests):
// Unit test: uses DataInlineUnit with DrnTestContextUnit (lightweight, no container overhead)
[Theory]
[DataInlineUnit(100)]
public void DataInlineUnitDemonstration(DrnTestContextUnit context, int maxLimit, IMockable autoInlinedDependency)
{
context.ServiceCollection.AddApplicationServices();
// Context-managed service resolution applies auto-inlined substitutes to matching dependencies
var dependentService = context.GetRequiredService<DependentService>();
autoInlinedDependency.Max.Returns(maxLimit); // Inlined data & NSubstitute mock
dependentService.Max.Should().Be(100);
}
// Integration test: uses DataInline with full DrnTestContext, ApplicationContext, and auto-mocking
[Theory]
[DataInline("/api/health", 100)]
public async Task DataInlineIntegrationDemonstration(DrnTestContext context, string endpoint, int maxLimit, IMockable autoInlinedDependency)
{
autoInlinedDependency.Max.Returns(maxLimit); // Dependency auto-mocked by NSubstitute and synced to ApplicationContext
// Builds application host with mocked services, binds dependencies/migrations, and creates HttpClient
var client = await context.ApplicationContext.CreateClientAsync<Program>();
var response = await client.GetAsync(endpoint);
response.Should().BeSuccessful();
}
Testing models used in the QuickStart
public static class ApplicationModule //Can be defined in Application Layer or in Hosted App
{
public static void AddApplicationServices(this IServiceCollection serviceCollection)
{
serviceCollection.AddTransient<IMockable, ToBeRemovedService>(); //default resolution will prefer the substitute requested by the test method
serviceCollection.AddTransient<DependentService>(); //dependent service uses IMockable and Max property returns dependency's Max value
}
}
public interface IMockable
{
public int Max { get; }
}
public class ToBeRemovedService : IMockable
{
public int Max { get; set; }
}
public class DependentService : IMockable
{
private readonly IMockable _mockable;
public DependentService(IMockable mockable)
{
_mockable = mockable;
}
public int Max => _mockable.Max;
}
QuickStart: Advanced
Advanced unit test example with inlined values, auto-generated data, and mocked interfaces:
DataInlineUnitprovidesDrnTestContextUnitas the first parameter when requested- Then it provides inlined values
- Then it auto-generates missing values with AutoFixture
AutoFixturemocks any interface parameter withNSubstitute
/// <param name="context"> Provided by DataInlineUnit even if it is not a compile time constant</param>
/// <param name="inlineData">Provided by DataInlineUnit</param>
/// <param name="autoInlinedData">DataInlineUnit will provide missing data with the help of AutoFixture</param>
/// <param name="autoInlinedMockable">DataInlineUnit will provide implementation mocked by NSubstitute</param>
[Theory]
[DataInlineUnit(99)]
public void TestContext_Should_Be_Created_From_DrnTestContextData(DrnTestContextUnit context, int inlineData, Guid autoInlinedData, IMockable autoInlinedMockable)
{
inlineData.Should().Be(99);
autoInlinedData.Should().NotBeEmpty(); //guid generated by AutoFixture
autoInlinedMockable.Max.Returns(int.MaxValue); //dependency mocked by NSubstitute
context.ServiceCollection.AddApplicationServices(); //you can add services, modules defined in hosted app, application, infrastructure layer etc..
var serviceProvider = context.BuildServiceProvider(); //settings.json added by convention. Context and service provider will be disposed by xunit
serviceProvider.GetRequiredService<IMockable>().Should().BeSameAs(autoInlinedMockable);
var dependentService = serviceProvider.GetRequiredService<DependentService>();
dependentService.Max.Should().Be(int.MaxValue);
}
DrnTestContext
DrnTestContext has following properties:
- captures values provided to running test method, test method info and location.
- provides
ServiceCollectionso that to be tested services and dependencies can be added before buildingServiceProvider. - provides and implements lightweight
ServiceProviderthat contains default logging without any providerServiceProvidercan provide services that depends on likeILogger<DefaultService>- logged data will not be leaked to anywhere since it has no logging provider.
- provides
ContainerContext- can start/bind
postgrescontainers, apply migrations for registeredDrnContexttypes, and update connection string configuration with a single line of code - exposes RabbitMQ as an explicit opt-in helper; RabbitMQ is not started by Postgres binding or
CreateClientAsync
- can start/bind
- provides
ApplicationContext- syncs
DrnTestContextservice collection and service provider with provided application by WebApplicationFactory - automatically captures application logs only while a debugger is attached and
Xunit.TestContext.Current.TestOutputHelperis available; otherwise, automatic logging remains disabled
- syncs
- provides
FlurlHttpTestfor mocking external HTTP requests (see FlurlHttpTest Integration) - provides
IConfigurationandIAppSettingswith SettingsProvider by using convention.- settings.json file can be found in the same folder with test
- settings.json file can be found in the global Settings folder or Settings folder that stays in the test folder
- Make sure file is copied to output directory
- If no settings file is specified while calling
BuildServiceProvider,settings.jsonis searched by convention.
- provides data file contents by using convention.
- data file can be found in the same folder with test
- data file can be found in the global Data folder or Data folder that stays in the test folder
- Make sure file is copied to output directory
- provides
MethodContext.GetTempPath()and context-levelGetTempPath()for a created, method-scoped temporary directory underAppConstants.TempPath. This directory is owned byDrnTestContext/DrnTestContextUnitand is automatically deleted during disposal (even if other cleanup steps fail), preventing directory leaks. - triggers
StartupJobRunnerto execute one-time test setup jobs marked withITestStartupJob ServiceProviderprovides utils provided with DRN.Framework.Utils'UtilsModule- Services resolved through the context use generated substitutes for interface or abstract types requested by the test.
ServiceProviderandDrnTestContextwill be disposed by xunit when test finishes- DI Health Check:
ValidateServicesAsync()ensures that attribute-registered services can be resolved without runtime errors.
settings.json can be put in the same folder that test file belongs. This way providing and isolating test settings is much easier
[Theory]
[DataInline( "localhost")]
public void DrnTestContext_Should_Add_Settings_Json_To_Configuration(DrnTestContext context, string value)
{
//settings.json file can be found in the same folder with test file, in the global Settings folder or Settings folder that stays in the same folder with test file
context.GetRequiredService<IAppSettings>().GetRequiredSection("AllowedHosts").Value.Should().Be(value);
}
data.txt can be put in the same folder that test file belongs. This way providing and isolating test data is much easier
[Theory]
[DataInline("data.txt", "Atatürk")]
[DataInline("alternateData.txt", "Father of Turks")]
public void DrnTestContext_Should_Return_Test_Specific_Data(DrnTestContext context, string dataPath, string data)
{
//data file can be found in the same folder with test file, in the global Data folder or Data folder that stays in the same folder with test file
context.GetData(dataPath).Data.Should().Be(data);
}
ContainerContext
With ContainerContext and conventions you can easily write effective integration tests against your database and message queue dependencies.
PostgreSQL Container
[Theory]
[DataInline]
public async Task QAContext_Should_Add_Category(DrnTestContext context)
{
context.ServiceCollection.AddSampleInfraServices();
await context.ContainerContext.Postgres.ApplyMigrationsAsync();
var qaContext = context.GetRequiredService<QAContext>();
var category = new Category("dotnet8");
qaContext.Categories.Add(category);
await qaContext.SaveChangesAsync();
category.Id.Should().BePositive();
}
- Application modules can be registered without any modification to
DrnTestContext DrnTestContext'sContainerContext- starts/binds the shared PostgreSQL container when requested, then scans DrnTestContext's service collection for inherited DrnContexts.
- Adds connection strings to DrnTestContext's configuration for each derived
DrnContextaccording to convention. - Disables Npgsql pooling in injected test connection strings so closed operations release physical sessions immediately instead of retaining them across parallel or delayed-theory hosts.
DrnTestContextacts as a ServiceProvider and when a service is requested it can build it from service collection with all dependencies.
RabbitMQ Container
You can start a RabbitMQ container for testing message queue integrations:
[Theory]
[DataInline]
public async Task RabbitMQ_Integration_Test(DrnTestContext context)
{
var container = await RabbitMQContext.StartAsync();
var connectionString = container.GetConnectionString();
// Use connectionString for your message queue tests
}
Advanced Container Configuration
For per-test customization, pass PostgresContainerSettings to an isolated container. Set
PostgresContext.PostgresContainerSettings only for a process-wide shared default, before the shared container is first initialized.
[Theory]
[DataInline]
public async Task Custom_Container_Verification(DrnTestContext context)
{
var settings = new PostgresContainerSettings
{
Database = "custom_db"
};
await context.ContainerContext.Postgres.Isolated.ApplyMigrationsAsync(settings);
// ...
}
Isolated Containers
By default, DrnTestContext shares a single Postgres container across tests for performance. For scenarios requiring complete isolation (e.g., changing global system state), use PostgresContextIsolated:
[Theory]
[DataInline]
public async Task Isolated_Test_Run(DrnTestContext context)
{
// Starts a FRESH, exclusive container for this test
var container = await context.ContainerContext.Postgres.Isolated.ApplyMigrationsAsync();
// ... use the isolated container ...
}
Rapid Prototyping (No Migrations)
For rapid development where migrations are not yet created, register the target DrnContext<TContext> through its application
or infrastructure module, then use EnsureDatabaseAsync to create the schema directly from the model:
await context.ContainerContext.Postgres.Isolated.EnsureDatabaseAsync<MyDrnContext>();
ApplicationContext
ApplicationContext syncs DrnTestContext service collection and configuration with a WebApplicationFactory.
- You can override configuration and services until the factory builds a host, such as when
CreateClient()orTestServeris requested. - Creating another application first disposes the current factory; the new application uses the current test configuration and service registrations.
CreateClientAsync<TProgram>()callsContainerContext.BindExternalDependenciesAsync(), which applies Postgres migrations for registeredDrnContexttypes. It does not start RabbitMQ.- When each application is created,
ApplicationContextautomatically captures logs only while a debugger is attached andXunit.TestContext.Current.TestOutputHelperis available; otherwise, automatic logging remains disabled. - The optional
ITestOutputHelperparameters onCreateApplicationAndBindDependenciesAsyncandCreateClientAsyncremain as explicit compatibility overrides and use the same debugger-only privacy gate. - By default, without debugger-enabled output logging, application lifecycle logs are not written to shared test-runner output.
TestEnvironment.DrnTestContextEnabled = trueidentifies test execution and prevents local development provisioning from colliding with integration tests.TemporaryApplicationis not a general test marker.
Basic Usage
[Theory]
[DataInline]
public async Task ApplicationContext_Should_Provide_Configuration_To_Program(DrnTestContext context)
{
var webApplication = context.ApplicationContext.CreateApplication<SampleProgram>();
await context.ContainerContext.Postgres.ApplyMigrationsAsync();
var client = webApplication.CreateClient();
var forecasts = await client.GetFromJsonAsync<WeatherForecast[]>("/Api/Sample/WeatherForecast");
forecasts.Should().NotBeNull();
var appSettingsFromWebApplication = webApplication.Services.GetRequiredService<IAppSettings>();
var connectionString = appSettingsFromWebApplication.GetRequiredConnectionString(nameof(QAContext));
connectionString.Should().NotBeNull();
var appSettingsFromDrnTestContext = context.GetRequiredService<IAppSettings>();
appSettingsFromWebApplication.Should().BeSameAs(appSettingsFromDrnTestContext);//resolved from same service provider
}
Simplified Client Creation
For most API testing scenarios, use CreateClientAsync which handles common setup:
[Theory]
[DataInline]
public async Task Simplified_API_Test(DrnTestContext context)
{
// Builds the app, binds Postgres dependencies, applies migrations, and returns an HttpClient
var client = await context.ApplicationContext.CreateClientAsync<Program>();
var response = await client.GetAsync("/api/endpoint");
response.Should().BeSuccessful();
}
Test Output Logging
ApplicationContext automatically captures application logs only while a debugger is attached and
Xunit.TestContext.Current.TestOutputHelper is available. If either condition is false, automatic logging remains disabled.
No constructor injection or helper argument is required:
[Theory]
[DataInline]
public async Task Test_With_Logging(DrnTestContext context)
{
var app = await context.ApplicationContext
.CreateApplicationAndBindDependenciesAsync<Program>();
// Automatic logs appear only with a debugger and an available current xUnit output helper
}
Do not declare ITestOutputHelper as a [DataInline] theory-method parameter for this purpose. AutoFixture creates an
NSubstitute value for interface parameters; that value is not xUnit's runner-owned helper. Existing callers may still
pass a real helper to the optional compatibility parameters, but new and updated callers should omit it.
Local Development Experience
DRN.Framework.Testing enhances local development by providing infrastructure management capabilities directly to the host application.
Setup
To use this feature in your main application (not in test projects), you must add a reference to DRN.Framework.Testing that is only active in Debug configuration. This prevents test dependencies from leaking into production builds.
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<ProjectReference Include="..\DRN.Framework.Testing\DRN.Framework.Testing.csproj" />
</ItemGroup>
LaunchExternalDependenciesAsync
This extension method on WebApplicationBuilder launches Postgres Testcontainers when the application starts in a development environment and the launch feature is enabled.
// In your DrnProgramActions implementation (e.g., SampleProgramActions.cs)
#if DEBUG
public override async Task ApplicationBuilderCreatedAsync<TProgram>(
TProgram program, WebApplicationBuilder builder,
IAppSettings appSettings, IScopedLog scopedLog)
{
var launchOptions = new ExternalDependencyLaunchOptions
{
PostgresContainerSettings = new PostgresContainerSettings
{
Reuse = true, // Keep container running across restarts
HostPort = 6432 // Bind to a specific port to avoid conflicts
}
};
// Automatically starts containers if they are not already running
await builder.LaunchExternalDependenciesAsync(scopedLog, appSettings, launchOptions);
}
#endif
Launch Conditions
LaunchExternalDependenciesAsync is designed to be safe and non-intrusive. It only executes when all following conditions are met:
- Environment: Must be
Development. - Launch Flag:
AppSettings.DevelopmentSettings.LaunchExternalDependenciesmust betrue. - Not in Test:
TestEnvironment.DrnTestContextEnabledmust befalse(prevents collision with test containers). - Not Temporary:
AppSettings.DevelopmentSettings.TemporaryApplicationmust befalse.
This feature is particularly useful for:
- Onboarding: New developers can run the app without manually setting up infrastructure.
- Consistency: Ensures all developers use the same infrastructure configuration.
- Rapid Prototyping: Quickly spin up throwaway databases.
Connection String Resolution
The framework uses different strategies for connection string resolution. See the current environment resolution table in DRN.Framework.EntityFramework README.
Key Scenarios
| Scenario | Connection Source |
|---|