DRN.Framework.Testing 0.9.5-preview007

Prefix Reserved
This is a prerelease version of DRN.Framework.Testing.
dotnet add package DRN.Framework.Testing --version 0.9.5-preview007
                    
NuGet\Install-Package DRN.Framework.Testing -Version 0.9.5-preview007
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DRN.Framework.Testing" Version="0.9.5-preview007" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DRN.Framework.Testing" Version="0.9.5-preview007" />
                    
Directory.Packages.props
<PackageReference Include="DRN.Framework.Testing" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DRN.Framework.Testing --version 0.9.5-preview007
                    
#r "nuget: DRN.Framework.Testing, 0.9.5-preview007"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DRN.Framework.Testing@0.9.5-preview007
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DRN.Framework.Testing&version=0.9.5-preview007&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=DRN.Framework.Testing&version=0.9.5-preview007&prerelease
                    
Install as a Cake Tool

master develop Quality Gate Status

Security Rating Maintainability Rating Reliability Rating Vulnerabilities Bugs Lines of Code Coverage

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 - WebApplicationFactory integration 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

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:

  • DataInline provides DrnTestContext as first parameter
  • Then it provides inlined values
  • Then it auto-generates missing values with AutoFixture
  • AutoFixture mocks any interface parameter with NSubstitute
/// <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 ServiceCollection so that to be tested services and dependencies can be added before building ServiceProvider.
  • provides and implements lightweight ServiceProvider that contains default logging without any provider
    • ServiceProvider can provide services that depends on like ILogger<DefaultService>
    • logged data will not be leaked to anywhere since it has no logging provider.
  • provides ContainerContext
    • can start/bind postgres containers, apply migrations for registered DrnContext types, 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
  • provides ApplicationContext
    • syncs DrnTestContext service collection and service provider with provided application by WebApplicationFactory
    • supports ITestOutputHelper integration for capturing application logs in test output
  • provides FlurlHttpTest for mocking external HTTP requests (see FlurlHttpTest Integration)
  • provides IConfiguration and IAppSettings with 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.json is 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 StartupJobRunner to execute one-time test setup jobs marked with ITestStartupJob
  • ServiceProvider provides utils provided with DRN.Framework.Utils' UtilsModule
  • BuildServiceProvider replaces dependencies that can be replaced with inlined interfaces.
  • ServiceProvider and DrnTestContext will 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's ContainerContext
    • 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 DrnContext according to convention.
  • DrnTestContext acts 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() or TestServer is requested.
  • CreateClientAsync<TProgram>() calls ContainerContext.BindExternalDependenciesAsync(), which applies Postgres migrations for registered DrnContext types. 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 = true
    • AppSettings.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:

  1. Environment: Must be Development.
  2. Launch Flag: AppSettings.DevelopmentSettings.LaunchExternalDependencies must be true.
  3. Not in Test: TestEnvironment.DrnTestContextEnabled must be false (prevents collision with test containers).
  4. Not Temporary: AppSettings.DevelopmentSettings.TemporaryApplication must be false.

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-password and DrnContext_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 containers
  • DrnTestContextEnabled = 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 DrnTestContext as 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 InlineData except they try to provide missing values with AutoFixture and NSubstitute
  • DataMember attribute works like xunit MemberData except they try to provide missing values with AutoFixture and NSubstitute
  • DataSelf attribute needs to be inherited by another class and should call AddRow method 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 as DataInline but provides DrnTestContextUnit.
  • [DataMemberUnit]: Same as DataMember but provides DrnTestContextUnit.
  • DataSelfUnitAttribute: Base class for custom self-contained data attributes that provide DrnTestContextUnit.

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 DrnTestContext or DrnTestContextUnit as 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.

  1. A data context attribute can provide NSubstituted interfaces and test context automatically replaces actual implementations with mocked interfaces and provides test data.
  2. Test containers can be used as actual dependencies instead of mocking them.
  3. 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.
  • DrnTestContext centralizes 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:


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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed

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