DRN.Framework.Testing
0.9.5-preview007
Prefix Reserved
dotnet add package DRN.Framework.Testing --version 0.9.5-preview007
NuGet\Install-Package DRN.Framework.Testing -Version 0.9.5-preview007
<PackageReference Include="DRN.Framework.Testing" Version="0.9.5-preview007" />
<PackageVersion Include="DRN.Framework.Testing" Version="0.9.5-preview007" />
<PackageReference Include="DRN.Framework.Testing" />
paket add DRN.Framework.Testing --version 0.9.5-preview007
#r "nuget: DRN.Framework.Testing, 0.9.5-preview007"
#:package DRN.Framework.Testing@0.9.5-preview007
#addin nuget:?package=DRN.Framework.Testing&version=0.9.5-preview007&prerelease
#tool nuget:?package=DRN.Framework.Testing&version=0.9.5-preview007&prerelease
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 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
Write your first auto-mocked test in seconds:
[Theory]
[DataInline]
public void DataInlineDemonstration(DrnTestContext context, IMockable autoInlinedDependency)
{
context.ServiceCollection.AddApplicationServices();
//Context wraps service provider and automagically replaces actual dependencies with auto inlined dependencies
var dependentService = context.GetRequiredService<DependentService>();
autoInlinedDependency.Max.Returns(int.MaxValue); //dependency is already mocked by NSubstitute
dependentService.Max.Should().Be(int.MaxValue); //That is all. It is clean and effective
}
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>(); //will be removed by test context because test method requested mocked interface
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 example with inlined values, auto-generated data, and mocked interfaces:
DataInlineprovidesDrnTestContextas first parameter- Then it provides inlined values
- Then it auto-generates missing values with AutoFixture
AutoFixturemocks any interface parameter withNSubstitute
/// <param name="context"> Provided by DataInline even if it is not a compile time constant</param>
/// <param name="inlineData">Provided by DataInline</param>
/// <param name="autoInlinedData">DataInline will provide missing data with the help of AutoFixture</param>
/// <param name="autoInlinedMockable">DataInline will provide implementation mocked by NSubstitute</param>
[Theory]
[DataInline(99)]
public void TestContext_Should_Be_Created_From_DrnTestContextData(DrnTestContext 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.GetService<ToBeRemovedService>().Should().BeNull(); //Service provider behaviour demonstration
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 - supports
ITestOutputHelperintegration for capturing application logs in test output
- 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
- triggers
StartupJobRunnerto execute one-time test setup jobs marked withITestStartupJob ServiceProviderprovides utils provided with DRN.Framework.Utils'UtilsModuleBuildServiceProviderreplaces dependencies that can be replaced with inlined interfaces.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).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.
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
You can customize the Postgres container before starting it using PostgresContainerSettings:
[Theory]
[DataInline]
public async Task Custom_Container_Verification(DrnTestContext context)
{
// Configure settings before accessing ContainerContext.Postgres
PostgresContext.PostgresContainerSettings = new PostgresContainerSettings
{
ContainerName = "my-custom-db",
Database = "custom_db",
HostPort = 5440 // Bind to specific host port
};
await context.ContainerContext.Postgres.ApplyMigrationsAsync();
// ...
}
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, use EnsureDatabaseAsync to create the schema directly from the model:
await context.ContainerContext.Postgres.Isolated.EnsureDatabaseAsync<MyDbContext>();
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. CreateClientAsync<TProgram>()callsContainerContext.BindExternalDependenciesAsync(), which applies Postgres migrations for registeredDrnContexttypes. It does not start RabbitMQ.LogToTestOutput(output, debuggerOnly: true)captures application logs only when the debugger is attached by default.- Test isolation flags keep local development provisioning from colliding with integration tests:
TestEnvironment.DrnTestContextEnabled=trueAppSettings.DevelopmentSettings.TemporaryApplication=true
Basic Usage
[Theory]
[DataInline]
public async Task ApplicationContext_Should_Provide_Configuration_To_Program(DrnTestContext context)
{
var webApplication = context.ApplicationContext.CreateApplication<Program>();
await context.ContainerContext.Postgres.ApplyMigrationsAsync();
var client = webApplication.CreateClient();
var forecasts = await client.GetFromJsonAsync<WeatherForecast[]>("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, ITestOutputHelper output)
{
// Builds the app, binds Postgres dependencies, applies migrations, and returns an HttpClient
var client = await context.ApplicationContext.CreateClientAsync<Program>(output);
var response = await client.GetAsync("/api/endpoint");
response.Should().BeSuccessful();
}
Test Output Logging
Capture application logs in test output for debugging:
[Theory]
[DataInline]
public async Task Test_With_Logging(DrnTestContext context, ITestOutputHelper output)
{
context.ApplicationContext.LogToTestOutput(output);
var app = context.ApplicationContext.CreateApplication<Program>();
// Application logs will appear in test output
}
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 comprehensive flow diagram in DRN.Framework.EntityFramework README.
Key Scenarios
| Scenario | Connection Source | Settings Used |
|---|---|---|
| Production/Staging | ConnectionStrings:{ContextName} |
Explicit config only |
| Local Debug | LaunchExternalDependenciesAsync() |
PostgresContainerSettings.DefaultPassword |
| Docker/K8s Dev | DrnContextDevelopmentConnection |
DbContextConventions->DrnContext_Dev* & postgres-password |
| DrnTestContext | ContainerContext.Postgres |
PostgresContainerSettings defaults |
Testcontainers Mode (tests or LaunchExternalDependencies = true):
postgres-passwordandDrnContext_Dev*settings are NOT used- Containers use the PostgreSQL defaults listed below
- Connection strings are automatically injected into configuration
Configuration Settings Reference
Settings for Docker/Kubernetes Development
These settings are used only by DrnContextDevelopmentConnection for containerized development. They are NOT used by ContainerContext or LaunchExternalDependencies.
| Setting | Default | Source |
|---|---|---|
DrnContext_DevHost |
drn |
DbContextConventions.DevHostKey |
DrnContext_DevPort |
5432 |
DbContextConventions.DevPortKey |
DrnContext_DevUsername |
drn |
DbContextConventions.DevUsernameKey |
DrnContext_DevDatabase |
drn |
DbContextConventions.DevDatabaseKey |
postgres-password |
(required) | DbContextConventions.DevPasswordKey |
Migration and Workflow Settings
Usually set by appsettings.Development.json, environment variables, or config maps.
| Setting | Default | Source | Purpose |
|---|---|---|---|
DrnDevelopmentSettings:AutoMigrateDevelopment |
true |
DrnDevelopmentSettings.AutoMigrateDevelopment | Auto-migrate in Development |
DrnDevelopmentSettings:AutoMigrateStaging |
false |
DrnDevelopmentSettings.AutoMigrateStaging | Auto-migrate in Staging; migrations only |
DrnDevelopmentSettings:Prototype |
false |
DrnDevelopmentSettings.Prototype | Development-only DB recreation on model changes |
DrnDevelopmentSettings:LaunchExternalDependencies |
false |
DrnDevelopmentSettings.LaunchExternalDependencies | Launch local PostgreSQL Testcontainers |
DrnDevelopmentSettings:TemporaryApplication |
false |
DrnDevelopmentSettings.TemporaryApplication | Auto-set by tests to prevent collision |
Container Defaults
PostgreSQL defaults used by ContainerContext.Postgres and LaunchExternalDependencies:
| Property | Default |
|---|---|
DefaultImage |
"postgres" |
DefaultVersion |
"18.4-alpine3.23" |
DefaultPassword |
"drn" |
Database |
"drn" |
Username |
"drn" |
RabbitMQ defaults used when a test explicitly calls RabbitMQContext.StartAsync():
| Property | Default |
|---|---|
DefaultImage |
"rabbitmq" |
DefaultVersion |
"4.2.3-management-alpine" |
Username |
unset |
Password |
unset |
RabbitMQ is explicit: it is not started by CreateClientAsync, BindExternalDependenciesAsync, or PostgreSQL binding.
Test Settings Convention
SettingsProvider uses settings.json by convention. It resolves settings from the test-local folder or a Settings/ folder and passes the selected base name to AddDrnSettings.
DrnDevelopmentSettings in Tests
DrnTestContext and ApplicationContext set test-isolation flags before application hosts are built:
// Automatically set by ApplicationContext.CreateApplication<TProgram>()
TestEnvironment.DrnTestContextEnabled = true;
AppSettings.DevelopmentSettings.TemporaryApplication = true;
What This Means:
TemporaryApplication = true: Prevents test runs from interfering with local dev containersDrnTestContextEnabled = true: Signals test context, enabling test-specific behaviors
See DrnDevelopmentSettings.cs for the complete class definition.
Data Attributes
DRN.Framework.Testing provides following data attributes that can provide data to tests:
- DataInlineAttribute
- DataMemberAttribute
- DataSelfAttribute
Following design principle is used for these attributes
- All attributes have data prefix to benefit from autocomplete
- All data attributes automatically provide
DrnTestContextas first parameter if tests requires - All data attributes try to provide missing values with AutoFixture and NSubstitute
- All data attributes will automatically override DrnTestContext's service collection with provided NSubstitute interfaces
- DataInline attribute works like xunit
InlineDataexcept they try to provide missing values with AutoFixture and NSubstitute - DataMember attribute works like xunit
MemberDataexcept they try to provide missing values with AutoFixture and NSubstitute - DataSelf attribute needs to be inherited by another class and should call
AddRowmethod in constructor to provide data
Resolution order is identical for integration and unit variants: optional context first, then inline/member/self-provided values, then AutoFixture-generated values, then NSubstitute mocks for interface or abstract parameters. Request DrnTestContext or DrnTestContextUnit only when the test uses it. Interface substitutes are for dependencies; instantiate the concrete class when the concrete convenience method itself is the behavior under test.
Example usages for DataMember attribute
[Theory]
[DataMember(nameof(DrnTestContextInlineMemberData))]
public void DrnTestContextMember_Should_Inline_And_Auto_Generate_Missing_Test_Data(DrnTestContext testContext,
int inline, ComplexInline complexInline, Guid autoGenerate, IMockable mock)
{
testContext.Should().NotBeNull();
testContext.MethodContext.TestMethod.Name.Should().Be(nameof(DrnTestContextMember_Should_Inline_And_Auto_Generate_Missing_Test_Data));
inline.Should().BeGreaterThan(10);
complexInline.Count.Should().BeLessThan(10);
autoGenerate.Should().NotBeEmpty();
mock.Max.Returns(75);
mock.Max.Should().Be(75);
}
public static IEnumerable<object[]> DrnTestContextInlineMemberData => new List<object[]>
{
new object[] { 11, new ComplexInline(8) },
new object[] { int.MaxValue, new ComplexInline(-1) }
};
Example usage for DataSelf attribute
public class DataSelfUnitAttributeTests
{
[Theory]
[DataSelfUnitTestData]
public void DrnTestContextClassData_Should_Inline_And_Auto_Generate_Missing_Test_Data(DrnTestContextUnit testContext,
int inline, ComplexInline complexInline, Guid autoGenerate, IMockable mock)
{
testContext.Should().NotBeNull();
testContext.MethodContext.TestMethod.Name.Should().Be(nameof(DrnTestContextClassData_Should_Inline_And_Auto_Generate_Missing_Test_Data));
inline.Should().BeGreaterThan(98);
complexInline.Count.Should().BeLessThan(1001);
autoGenerate.Should().NotBeEmpty();
mock.Max.Returns(44);
mock.Max.Should().Be(44);
}
}
public class DataSelfUnitTestData : DataSelfUnitAttribute
{
public DataSelfUnitTestData()
{
AddRow(99, new ComplexInline(100));
AddRow(199, new ComplexInline(1000));
}
}
Example usage for DataInline attribute
[Theory]
[DataInline(99)]
public void TestContext_Should_Be_Created_From_DrnTestContextData(DrnTestContext 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.GetService<ToBeRemovedService>().Should().BeNull(); //Service provider behaviour demonstration
var dependentService = serviceProvider.GetRequiredService<DependentService>();
dependentService.Max.Should().Be(int.MaxValue);
}
Unit Testing
For pure unit tests that do not need container orchestration or full application startup, use DrnTestContextUnit and the corresponding Unit attributes.
Unit Attributes
[DataInlineUnit]: Same asDataInlinebut providesDrnTestContextUnit.[DataMemberUnit]: Same asDataMemberbut providesDrnTestContextUnit.DataSelfUnitAttribute: Base class for custom self-contained data attributes that provideDrnTestContextUnit.
DrnTestContextUnit
Unlike DrnTestContext, DrnTestContextUnit is lightweight and focused on method data, method metadata, configuration, and optional unit-level service validation. It does not provide ContainerContext, ApplicationContext, FlurlHttpTest, or full app startup.
[Theory]
[DataInlineUnit(99)]
public void Unit_Test_Example(DrnTestContextUnit context, int value, IMockable mock)
{
// Fast, lightweight, no container overhead
context.MethodContext.TestMethod.Name.Should().Be(nameof(Unit_Test_Example));
mock.Max.Returns(value);
var service = new DependentService(mock); // Manually inject dependencies
service.Max.Should().Be(99);
}
Test Consolidation
If tests share the same setup and their consolidation creates no semantic or performance issue, they should be unified. Apply when consolidation requires only minimal essential change.
Parameterized
When multiple test cases share identical test bodies and differ only in input/expected-output, consolidate them into a single [Theory] with multiple data attribute rows instead of writing separate methods.
Anti-pattern — separate methods for each case:
[Theory]
[DataInlineUnit]
public void Add_Should_Return_Positive_Sum(DrnTestContextUnit context)
{
var calc = new Calculator();
calc.Add(2, 3).Should().Be(5);
}
[Theory]
[DataInlineUnit]
public void Add_Should_Handle_Negatives(DrnTestContextUnit context)
{
var calc = new Calculator();
calc.Add(-1, -2).Should().Be(-3);
}
[Theory]
[DataInlineUnit]
public void Add_Should_Handle_Zero(DrnTestContextUnit context)
{
var calc = new Calculator();
calc.Add(0, 0).Should().Be(0);
}
Preferred — one parameterized method covering all permutations:
[Theory]
[DataInlineUnit(2, 3, 5)] // positive + positive
[DataInlineUnit(-1, -2, -3)] // negative + negative
[DataInlineUnit(0, 0, 0)] // zeros
[DataInlineUnit(-1, 1, 0)] // cancellation
public void Add_Should_Return_Correct_Sum(DrnTestContextUnit context, int a, int b, int expected)
{
var calc = new Calculator();
calc.Add(a, b).Should().Be(expected);
}
Flow
When tests share identical setup (container init, migrations, service registration) and additional assertions can be applied by continuing the existing test flow, unify into a single test. Prevents code duplication, maintenance burden, and redundant setup/teardown cost. Most valuable in integration tests where setup is expensive.
Reference: QAContextTagTests.cs — single test flow validating entity IDs, JSON model queries, date filters, and materialization interceptor with one shared setup.
Guidelines
- Last parameter = expected result — each attribute row is a self-contained specification
- Name covers the dimension — describes what is tested, not a specific case
- Comment inline data — add trailing comments when values aren't self-explanatory
- Extract shared setup — use private helpers to keep the test body focused on act + assert
- Omit values for auto-generated params — let AutoFixture/NSubstitute handle params you don't control
- Omit context when unused — declare
DrnTestContextorDrnTestContextUnitas a parameter only when the test body uses it; omit it for pure logic tests that need no context - Don't consolidate when test bodies differ structurally or separate failure messages aid debugging more than parameterization
DebugOnly Tests
Following attributes can be used to run test only when the debugger is attached. These attributes does respect the attached debugger, not debug or release configuration.
- FactDebuggerOnly
- TheoryDebuggerOnly
DI Health Validation
Use ValidateServicesAsync() to catch missing dependencies for attribute-registered services before they fail your application at runtime.
[Theory]
[DataInline]
public async Task Dependency_Injection_Should_Be_Healthy(DrnTestContext context)
{
context.ServiceCollection.AddApplicationServices();
// Verifies that attribute-registered services can be successfully resolved
await context.ValidateServicesAsync();
}
JSON Utilities
The JsonObjectExtensions provide a simple way to verify API contracts and serialization stability.
ValidateObjectSerialization
Ensures that an object can be serialized to JSON and deserialized back to an equivalent object.
[Theory]
[DataInline]
public void Contract_Should_RoundTrip_Successfully(MyContractDto dto)
{
// AutoFixture fills dto, then we verify round-trip
dto.ValidateObjectSerialization();
}
FlurlHttpTest Integration
DrnTestContext provides built-in support for mocking HTTP requests via Flurl.Http.Testing. This enables testing services that make external API calls without hitting real endpoints.
Basic Usage
[Theory]
[DataInline]
public async Task External_API_Should_Be_Mocked(DrnTestContext context)
{
// Setup mock response
context.FlurlHttpTest.RespondWith("{ \"status\": \"ok\" }", 200);
context.ServiceCollection.AddSingleton<IExternalApiClient, ExternalApiClient>();
var client = context.GetRequiredService<IExternalApiClient>();
var result = await client.GetStatusAsync();
result.Status.Should().Be("ok");
// Verify the request was made
context.FlurlHttpTest.ShouldHaveCalled("https://api.example.com/status")
.WithVerb(HttpMethod.Get)
.Times(1);
}
Simulating Failures
[Theory]
[DataInline]
public async Task Service_Should_Handle_API_Failure(DrnTestContext context)
{
// Simulate server error
context.FlurlHttpTest.RespondWith(status: 500);
context.ServiceCollection.AddSingleton<IExternalApiClient, ExternalApiClient>();
var client = context.GetRequiredService<IExternalApiClient>();
var act = async () => await client.GetStatusAsync();
await act.Should().ThrowAsync<FlurlHttpException>();
}
Sequential Responses
[Theory]
[DataInline]
public async Task Retry_Logic_Should_Work(DrnTestContext context)
{
// First call fails, second succeeds (testing retry logic)
context.FlurlHttpTest
.RespondWith(status: 503)
.RespondWith("{ \"status\": \"ok\" }", 200);
// ... test retry behavior
}
Providers
SettingsProvider
SettingsProvider gets the settings from Settings folder. Settings file path is relative Settings folder. Settings folder must be created in the root of the test Project. Make sure the settings file is copied to output directory.
[Fact]
public void SettingsProvider_Should_Return_IAppSettings_Instance()
{
var appSettings = SettingsProvider.GetAppSettings();
appSettings.GetRequiredSection("AllowedHosts").Value.Should().Be("*");
appSettings.TryGetSection("Bar", out _).Should().BeTrue();
appSettings.TryGetSection("Foo", out _).Should().BeFalse();
appSettings.GetRequiredConnectionString("Foo").Should().Be("Bar");
appSettings.TryGetConnectionString("Bar", out _).Should().BeFalse();
}
[Fact]
public void SettingsProvider_Should_Return_IConfiguration_Instance()
{
var configuration = SettingsProvider.GetConfiguration("secondaryAppSettings");
configuration.GetRequiredSection("AllowedHosts").Value.Should().Be("*");
configuration.GetSection("Foo").Exists().Should().BeTrue();
configuration.GetSection("Bar").Exists().Should().BeFalse();
configuration.GetConnectionString("Bar").Should().Be("Foo");
}
DataProvider
DataProvider gets the content of specified data file in the Data folder. Data file path is relative Data folder including file extension. Data folder must be created in the root of the test Project. Make sure the data file is copied to output directory.
[Fact]
public void DataProvider_Should_Return_Data_From_Test_File()
{
DataProvider.Get("Test.txt").Should().Be("Foo");
}
CredentialsProvider
CredentialsProvider is a helper class for generating and caching test usernames and passwords.
[Fact]
public void CredentialsProvider_Should_Generate_Test_User()
{
var credentials = CredentialsProvider.GenerateCredentials();
credentials.Username.Should().StartWith("testuser_");
credentials.Password.Length.Should().BeGreaterThanOrEqualTo(12);
}
xUnit Runner Configuration
xunit.runner.json is optional but recommended for configuring the test runner. When using Microsoft Testing Platform (MTP), this file ensures the runner behaves as expected (e.g., parallelization settings). Ensure this file is set to CopyToOutputDirectory in your csproj.
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"diagnosticMessages": true,
"parallelizeAssembly": true,
"parallelizeTestCollections": true
}
MTP Execution
Run test projects directly with Microsoft Testing Platform. Do not use .slnx for test execution; run unit tests first, then integration tests only after unit tests pass.
dotnet run --project DRN.Test.Unit/DRN.Test.Unit.csproj
dotnet run --project DRN.Test.Integration/DRN.Test.Integration.csproj
For WebApplicationFactory<TProgram> scenarios, point TProgram at a hosted application or a non-test Web SDK support assembly such as DRN.Test.Utils (IsTestProject=false). Keep custom disposable app entry points out of the MTP test executable; keep test assertions in DRN.Test.Integration.
Example Test Project .csproj File
Don't forget to replace DRN.Framework.Testing project reference with its nuget package reference
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<OutputType>Exe</OutputType>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.v3.mtp-v2" Version="3.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DRN.Framework.Testing\DRN.Framework.Testing.csproj"/>
</ItemGroup>
<ItemGroup>
<None Update="Settings\settings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Data\Test.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Settings\secondaryAppSettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="xunit.runner.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Test Snippet
dtt snippet for creating tests with a test context.
[Theory]
[DataInline]
public async Task $name$(DrnTestContext context)
{
$END$
}
Testing Guide and DTT Approach
DTT (Duran's Testing Technique) is a context-oriented testing approach developed to make testing a natural part of software development. Instead of scattering setup across fixtures, factories, and lifecycle hooks, DTT places a single test context at the center of the test. The context adapts to the test's scope. It is lightweight for unit tests (DrnTestContextUnit), full-stack for integration tests (DrnTestContext).
DTT is built upon two core ideas:
- Writing a unit or integration test, providing settings and data to it should be easy, effective and encouraging as much as possible
- A test should test actual usage as much as possible
DTT with DrnTestContext makes these ideas possible by
- being aware of test data and location
- effortlessly providing test data and settings
- effortlessly providing service collection
- effortlessly providing service provider
- effortlessly validating service provider
- effortlessly wiring external dependencies with Container Context
- effortlessly wiring application with Application Context
The context is opt-in: declare it as a parameter when the test needs it, omit it for pure logic tests that require no context. Data attributes inject the context only when the method signature requests it.
With the help of test context, integration tests can be written easily with following styles.
- A data context attribute can provide NSubstituted interfaces and test context automatically replaces actual implementations with mocked interfaces and provides test data.
- Test containers can be used as actual dependencies instead of mocking them.
- With FactDebuggerOnly and TheoryDebuggerOnly attributes, cautiously written tests can use real databases and dependencies to debug production usage.
Comparison
Without DTT: Manual setup requires significant boilerplate to achieve dependency injection, mocking, and data generation.
[Fact]
public void Manual_Setup_Boilerplate_Fatigue()
{
// 1. Setup DI container
var services = new ServiceCollection();
// 2. Manual Mocking
var mockDependency = Substitute.For<IMockable>();
mockDependency.Max.Returns(99);
services.AddSingleton(mockDependency);
services.AddTransient<DependentService>();
// 3. Manual Data Generation
var fixture = new Fixture();
var autoGeneratedId = fixture.Create<Guid>();
// 4. Build Provider
var serviceProvider = services.BuildServiceProvider();
var systemUnderTest = serviceProvider.GetRequiredService<DependentService>();
// 5. Test logic...
}
With DTT: The same setup is handled declaratively, allowing you to focus immediately on the test logic.
[Theory]
[DataInline(99)]
public void DTT_Pit_Of_Success(DrnTestContext context, int value, IMockable mockDependency, Guid autoGeneratedId)
{
// DI, Mocking, and Data Generation are already done.
mockDependency.Max.Returns(value);
var systemUnderTest = context.GetRequiredService<DependentService>();
// ...
}
Scenario 2: Integration Testing (Containers + WebApp + Migrations)
Without DTT: Setting up a realistic integration test with Testcontainers, EF Core Migrations, and WebApplicationFactory requires understanding the lifecycle of multiple complex components.
public class Complex_Integration_Test : IAsyncLifetime
{
private PostgreSqlContainer _container;
private WebApplicationFactory<Program> _factory;
public async Task InitializeAsync()
{
// 1. Spin up Container
_container = new PostgreSqlBuilder().WithImage("postgres:15").Build();
await _container.StartAsync();
// 2. Configure WebApp to use Container
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, config) =>
{
config.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("ConnectionStrings:Default", _container.GetConnectionString())
});
});
});
// 3. Apply Migrations Manually
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
await dbContext.Database.MigrateAsync();
}
[Fact]
public async Task Manual_Integration_Pain()
{
var client = _factory.CreateClient();
// Test logic...
}
public async Task DisposeAsync()
{
await _factor.DisposeAsync();
await _container.DisposeAsync();
}
}
With DTT: DrnTestContext handles the entire lifecycle orchestration for you.
[Theory]
[DataInline]
public async Task DTT_Full_Integration_Magic(DrnTestContext context, ITestOutputHelper output)
{
// One line to rule them all:
// 1. Binds Postgres dependencies
// 2. Wires up connection strings to overrides
// 3. Applies EF Core Migrations
// 4. Bootstraps WebApplicationFactory
// RabbitMQ is explicit: call RabbitMQContext.StartAsync() when a test needs it.
var client = await context.ApplicationContext.CreateClientAsync<Program>(output);
// Ready to test immediately
}
DTT Design Goal
DTT reduces test setup friction so tests can focus on behavior instead of repeated infrastructure wiring.
[DataInline]and[DataInlineUnit]provide context, generated data, and mocked interfaces declaratively.DrnTestContextcentralizes DI, settings/data lookup, external dependency binding, and application startup helpers.- Integration tests can use real PostgreSQL and application pipelines without per-test container or factory boilerplate.
The result is a consistent path for writing isolated unit tests and realistic integration tests with minimal ceremony.
Global Usings
global using Xunit;
global using Xunit.v3;
global using AutoFixture;
global using AutoFixture.AutoNSubstitute;
global using AutoFixture.Xunit3;
global using AwesomeAssertions;
global using NSubstitute;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.DependencyInjection.Extensions;
global using Microsoft.Extensions.Configuration;
global using DRN.Framework.Testing;
global using DRN.Framework.Testing.Contexts;
global using DRN.Framework.Testing.Contexts.Postgres;
global using DRN.Framework.Testing.Contexts.RabbitMQ;
global using DRN.Framework.Testing.DataAttributes;
global using DRN.Framework.Testing.Providers;
global using DRN.Framework.Testing.TestAttributes;
global using DRN.Framework.Utils.Extensions;
global using DRN.Framework.Utils.Settings;
global using DRN.Framework.SharedKernel;
global using DRN.Framework.Utils.DependencyInjection;
global using System.Reflection;
global using System.IO;
global using System.Linq;
global using System.Collections;
Telemetry Opt-Out
Add the following to your shell profile (e.g., ~/.zshrc or ~/.bashrc) to opt out of telemetry:
# Opt out of .NET CLI telemetry
export DOTNET_CLI_TELEMETRY_OPTOUT=1
# Opt out of .NET Testing Platform telemetry
export TESTINGPLATFORM_TELEMETRY_OPTOUT=1
References:
Related Packages
- DRN.Framework.SharedKernel - Domain primitives and exceptions
- DRN.Framework.Utils - Configuration and DI utilities
- DRN.Framework.EntityFramework - EF Core integration
- DRN.Framework.Hosting - Web application hosting
For complete examples, see Sample.Hosted.
Documented with the assistance of DiSCOS
Semper Progressivus: Always Progressive
Commit Info
Author: Duran Serkan
Date: 2026-06-13 21:37:18 +0900
Hash: 4ab8ce703e9aeed7534102cc01c8d11ee9b34022
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- AutoFixture.AutoNSubstitute (>= 4.18.1)
- AutoFixture.Xunit3 (>= 4.19.0)
- AwesomeAssertions (>= 9.4.0)
- DRN.Framework.EntityFramework (>= 0.9.5-preview007)
- DRN.Framework.Hosting (>= 0.9.5-preview007)
- Microsoft.AspNetCore.Mvc.Testing (>= 10.0.9)
- NSubstitute (>= 5.3.0)
- NSubstitute.Analyzers.CSharp (>= 1.0.17)
- Testcontainers.PostgreSql (>= 4.12.0)
- Testcontainers.RabbitMq (>= 4.12.0)
- xunit.v3.extensibility.core (>= 3.2.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.9.5-preview007 | 0 | 6/13/2026 |
| 0.9.5-preview006 | 89 | 6/7/2026 |
| 0.9.5-preview005 | 91 | 6/7/2026 |
| 0.9.5-preview004 | 87 | 6/7/2026 |
| 0.9.5-preview003 | 89 | 6/6/2026 |
| 0.9.5-preview002 | 93 | 6/2/2026 |
| 0.9.5-preview001 | 91 | 6/1/2026 |
| 0.9.4 | 103 | 5/13/2026 |
| 0.9.3 | 127 | 4/25/2026 |
| 0.9.2 | 109 | 4/18/2026 |
| 0.9.1 | 117 | 3/26/2026 |
| 0.9.0 | 111 | 3/25/2026 |
| 0.9.0-preview001 | 113 | 3/22/2026 |
| 0.8.0 | 141 | 3/14/2026 |
| 0.7.0 | 112 | 3/8/2026 |
| 0.7.0-preview067 | 107 | 3/7/2026 |
| 0.7.0-preview066 | 116 | 2/28/2026 |
| 0.7.0-preview065 | 114 | 2/25/2026 |
| 0.7.0-preview064 | 118 | 2/22/2026 |
| 0.7.0-preview063 | 121 | 2/21/2026 |
Not every version includes changes, features or bug fixes. This project can increment version to keep consistency with other DRN.Framework projects.
## Version 0.9.5
### Changed
* **PostgreSQL Testcontainer Default**: `PostgresContainerSettings.DefaultVersion` now uses `18.4-alpine3.23`.
## Version 0.9.4
Dependencies upgraded to dotnet 10.0.8
## Version 0.9.3
Dependencies upgraded to dotnet 10.0.7
## Version 0.9.2
Dependencies upgraded to dotnet 10.0.6
## Version 0.9.1
My family celebrates the enduring legacy of Mustafa Kemal Atatürk's enlightenment ideals and is proud to inherit his spiritual legacy: 'I am not leaving behind any definitive text, any dogma, any frozen, rigid rule as my spiritual legacy. My spiritual wealth is science and reason. Those who wish to embrace me after my death will become my spiritual heirs if they accept the guidance of reason and science on this fundamental axis.'
## Version 0.9.0
My family celebrates the enduring legacy of Mustafa Kemal Atatürk's enlightenment ideals and stands behind his remarkable words: 'Peace at home, peace in the world.'
## Version 0.8.0
My family celebrates the enduring legacy of Mustafa Kemal Atatürk's enlightenment ideals, rooted in his timeless words that 'science is the truest guide in life.' In that spirit, and to honor the 14 March Scientists Day, this release is dedicated to the researchers working for the benefit of humanity, and to the rejection of my first academic paper :) ([JOSS #10176](https://github.com/openjournals/joss-reviews/issues/10176)).
## Version 0.7.0
My family celebrates the enduring legacy of Mustafa Kemal Atatürk's enlightenment ideals and honors 8 March, International Women's Day, a cause inseparable from his vision of equality. This release is dedicated to freedom of speech, democracy, women's rights, and Prof. Dr. Ümit Özdağ, a defender of Mustafa Kemal Atatürk’s enlightenment ideals.
> [!WARNING]
> Since v0.6.0 (released 10 November 2024), substantial changes have occurred. This release notes file has been reset to reflect the current state of the project as of 08 March 2026. Previous history has been archived to maintain a clean source of truth based on the current codebase.
### New Features
* **DrnTestContext & DTT**
* **Full Integration Context**: `DrnTestContext` provides `ServiceCollection`, `ServiceProvider`, `Configuration`, and `FlurlHttpTest`.
* **Auto-Registration**: Automatically adds `DrnUtils` and executes `[StartupJob]`s for one-time setups.
* **Method Context**: Captures metadata for folder-based settings resolution.
* **DI Validation**: `ValidateServicesAsync()` for verifying service collection health and identifying missing dependencies early.
* **Lightweight Unit Context**: `DrnTestContextUnit` for pure unit tests without container overhead.
* **Container Orchestration**
* **ContainerContext**: Integrated PostgreSQL Testcontainer binding for registered `DrnContext`s; RabbitMQ is available through an explicit opt-in helper.
* **Auto-Wiring**: Scans for `DrnContext`s, creates PostgreSQL containers, applies migrations, and injects connection strings automatically.
* **Modes**: Supports shared containers (fast) or `.Isolated` containers (independent data).
* **Rapid Prototyping**: `EnsureDatabaseAsync` for schema generation without migrations.
* **Application Integration**
* **ApplicationContext**: Deep integration with `WebApplicationFactory`.
* **Helpers**: `CreateClientAsync` (starts app + migrations + auth client), `CreateApplicationAndBindDependenciesAsync`, `LogToTestOutput`.
* **Local Development Experience**
* **Infrastructure Management**: `LaunchExternalDependenciesAsync` for `WebApplicationBuilder` to automatically start PostgreSQL containers when `IsDevelopmentEnvironment` is true; RabbitMQ tests call `RabbitMQContext.StartAsync()` explicitly.
* **Data Attributes (Auto-Mocking)**
* **DataInline**: Replaces `[InlineData]`. Auto-mocks interfaces (NSubstitute), fills missing params (AutoFixture), provides `DrnTestContext`.
* **DataMember**: Replaces `[MemberData]`. Source data from properties with auto-mocking support.
* **DataSelf**: Self-contained test data classes inheriting `DataSelfAttribute` (using `AddRow`).
* **Debugger Attributes**: `[FactDebuggerOnly]` and `[TheoryDebuggerOnly]` for running tests only during debugging sessions.
* **Providers & Utilities**
* **SettingsProvider**: Loads `settings.json` and overrides from `Settings/` folder or test-local folder.
* **DataProvider**: Loads test data files (e.g., `.json`, `.txt`) from `Data/` folder or test-local folder.
* **CredentialsProvider**: Generates unique, consistent usernames/passwords for test authentication.
* **JSON Utilities**: `ValidateObjectSerialization<T>()` for one-line JSON round-trip contract verification.
---
Documented with the assistance of [DiSCOS](https://github.com/duranserkan/DRN-Project/blob/develop/DiSCOS/DiSCOS.md)
---
**Semper Progressivus: Always Progressive**
## Commit Info
Author: Duran Serkan
Date: 2026-06-13 21:37:18 +0900
Hash: 4ab8ce703e9aeed7534102cc01c8d11ee9b34022