๐๏ธ Core Concepts
April 27, 2026 ยท View on GitHub
NotoriousTest provides a structured way to manage test infrastructures and ensure clean, isolated integration tests. This document introduces the core concepts and how they work together.
Summary
- Infrastructure
- Extensions
- Configuration Propagation
- Test Environment
- Lifecycle
- Logging
- DoggyDog
- Accessing Infrastructures in Tests
๐๏ธ What is an Infrastructure?
An infrastructure represents an external dependency required for testing. This could be a database, message queue, file storage, external API, or any service that needs to be set up and managed during tests.
public class MyInfrastructure : Infrastructure
{
public MyInfrastructure(EnvironmentId contextId, ITestLogger logger, IRegistry registry)
: base(contextId, logger, registry) { }
public override Task Initialize()
{
// Called once at the start of the test session
return Task.CompletedTask;
}
public override Task Reset()
{
// Called before each test โ restore a clean state
return Task.CompletedTask;
}
public override Task Destroy()
{
// Called at the end of the session โ release resources
return Task.CompletedTask;
}
}
๐ Key Points:
Initialize()โ Called once at the start of the test session.Reset()โ Called before each test to ensure data isolation.Destroy()โ Called at the end of the test session to clean up resources.- The constructor receives
EnvironmentId,ITestLogger, andIRegistryโ all injected automatically by the DI container.
Note: All
Async-prefixed classes (AsyncInfrastructure,AsyncEnvironment, etc.) have been removed in v4.0.0. UseInfrastructure,Environment,IntegrationTestdirectly.
โ๏ธ Configuration Propagation
OutputConfigurationExtension and Infrastructure<TConfig, TMetadata> are built on top of two simple interfaces that define how infrastructures communicate configuration to each other:
// Produces configuration entries for other infrastructures
public interface IConfigurationProducer
{
List<ConfigurationEntry<object>> OutputConfiguration { get; }
}
// Receives the aggregated configuration entries from all producers
public interface IConfigurationConsumer
{
List<ConfigurationEntry<object>> ConsumedConfiguration { get; set; }
}
The environment orchestrates the propagation automatically:
- After all infrastructures are registered, it collects every
ConfigurationEntryfrom everyIConfigurationProducer. - Before initializing any
IConfigurationConsumer, it injects the full aggregated list intoConsumedConfiguration. IConfigurationConsumerinfrastructures always initialize last within their order group, so producers are guaranteed to be ready first.
This means you can implement IConfigurationConsumer directly on any infrastructure that needs to read entries produced by others โ not just WebApplicationInfrastructure:
public class MyConsumerInfrastructure : Infrastructure, IConfigurationConsumer
{
public List<ConfigurationEntry<object>> ConsumedConfiguration { get; set; } = [];
public override Task Initialize()
{
var entry = ConsumedConfiguration.FirstOrDefault(e => e.Key == "ConnectionStrings:MyDb");
// use entry.Value ...
return Task.CompletedTask;
}
}
๐ What is a Test Environment?
A test environment is a collection of infrastructures that define the full setup for running tests. It manages the DI container, lifecycle, and DoggyDog watchdog.
Extend the class from your framework-specific package:
// xUnit
public class MyTestEnvironment : NotoriousTest.XUnit.Environment
{
public MyTestEnvironment(IMessageSink sink) : base(sink) { }
public override Assembly CurrentAssembly => Assembly.GetExecutingAssembly();
public override async Task ConfigureEnvironment()
{
// Infrastructures are created via DI โ constructor parameters are injected automatically
AddInfrastructure<MyInfrastructure>();
AddInfrastructure<MyOtherInfrastructure>();
}
}
| Framework | Base class |
|---|---|
| xUnit | NotoriousTest.XUnit.Environment |
| NUnit | NotoriousTest.NUnit.Environment |
| MSTest | NotoriousTest.MSTest.Environment |
| TUnit | NotoriousTest.TUnit.Environment |
๐ Key Points:
AddInfrastructure<T>()resolvesTthrough the DI container โ all constructor parameters are injected.AddInfrastructure(new MyInfrastructure(...))can be used to pass a manually created instance.- Infrastructures run in parallel when they share the same
Ordervalue. IConfigurationConsumeralways run in last in their order group.
Dependency Injection
Environments expose an internal DI container. Override ConfigureInfrastructureServices to register your own services, which will then be injected into any infrastructure constructor:
public class MyTestEnvironment : NotoriousTest.XUnit.Environment
{
public override Assembly CurrentAssembly => Assembly.GetExecutingAssembly();
public override void ConfigureInfrastructureServices(IServiceCollection services)
{
base.ConfigureInfrastructureServices(services); // always call base
// Register custom services โ available in all infrastructure constructors
services.AddSingleton<IMyService, MyService>();
services.AddSingleton<MyOptions>(new MyOptions { ... });
}
public override async Task ConfigureEnvironment()
{
AddInfrastructure<MyInfrastructure>(); // receives IMyService via constructor injection
}
}
Available by default (no registration needed):
EnvironmentIdโ unique identifier for this test sessionITestSettingsProviderโ loadstestsettings.jsonITestLoggerโ framework-specific loggerIRegistryโ infrastructure registry (used by DoggyDog)
Output Configuration & Web
When using NotoriousTest.Web, call this.AddWebApplication<T>() to register your web application. It automatically receives all configuration entries produced by other infrastructures:
public override async Task ConfigureEnvironment()
{
AddInfrastructure<MyDatabaseInfrastructure>(); // produces connection strings
this.AddWebApplication<MyTestWebApp>(); // consumes them automatically
}
๐ How Does the Test Lifecycle Work?
NotoriousTest manages your test lifecycle automatically to ensure clean and isolated tests.
Test Session Starts
โโ DI container is built
โโ Registry is set up
โโ DoggyDog watchdog is launched
โโ ConfigureEnvironment() is called
โโ Infrastructures are initialized (Initialize()), in Order, in parallel when Order is equal
Before Each Test
โโ Infrastructures are reset (Reset())
Test Runs
โโ Test executes in an isolated environment
After Test Suite Completes
โโ Infrastructures are destroyed (Destroy())
โโ DoggyDog receives success signal โ no cleanup needed
๐ฅ Why is this important?
โ
Guarantees clean data for each test (no unwanted side effects).
โ
Prevents state leaks between tests.
โ
Eliminates manual setup/teardown code in test classes.
Controlling initialization order:
public class MyInfrastructure : Infrastructure
{
public override int? Order => 1; // lower runs first; null = unordered
}
Disabling reset for a specific infrastructure:
public class MyInfrastructure : Infrastructure
{
public override bool AutoReset => false; // Reset() will never be called
}
๐ Logging
Every infrastructure has access to a Logger property of type ITestLogger, injected automatically. It routes log output to the test framework's native output (console, test output window, etc.).
public class MyInfrastructure : Infrastructure
{
public MyInfrastructure(EnvironmentId contextId, ITestLogger logger, IRegistry registry)
: base(contextId, logger, registry) { }
public override Task Initialize()
{
Logger.Log("Initializing MyInfrastructure...");
return Task.CompletedTask;
}
}
ITestLogger is also registered in the DI container, so you can inject it into any service registered via ConfigureInfrastructureServices.
๐ถ DoggyDog
DoggyDog is a watchdog process that cleans up infrastructures left in a dirty state when a test campaign is killed unexpectedly (crash, forced termination, IDE stop, etc.).
How it works
- When a test session starts, the environment launches DoggyDog as a child process.
- Every infrastructure registers itself in a local SQLite registry when it initializes.
- DoggyDog monitors the test process. If the process exits without sending a success signal, DoggyDog runs the cleaner for each registered infrastructure.
- On a clean exit, the environment sends a success signal and DoggyDog shuts down without doing anything.
To locate cleaner implementations on crash recovery, DoggyDog scans the test assembly via reflection. This is why every environment requires a CurrentAssembly property:
public override Assembly CurrentAssembly => Assembly.GetExecutingAssembly();
DoggyDog uses this assembly to resolve the [Cleaner(typeof(T))] types at runtime. Without it, cleaners would not be found and crash recovery would be a no-op.
Defining a Cleaner
Annotate your infrastructure with [Cleaner(typeof(T))] and implement IInfrastructureCleaner<TMetadata>:
[Cleaner(typeof(MyInfrastructureCleaner))]
public class MyInfrastructure : Infrastructure<object, MyMetadata>
{
public override async Task Initialize()
{
// Store cleanup metadata so DoggyDog can clean up if we crash
Metadata = new MyMetadata { ContainerName = "my-container" };
}
}
public class MyInfrastructureCleaner : IInfrastructureCleaner<MyMetadata>
{
public async Task CleanAfterCrash(EnvironmentId contextId, Guid infrastructureId, MyMetadata? metadata)
{
if (metadata is null) return;
// e.g., stop and remove a Docker container
await StopContainer(metadata.ContainerName);
}
}
๐ Key Points:
Metadatais serialized to the registry when the infrastructure initializes and passed back to the cleaner on crash recovery.- If an infrastructure does not need crash cleanup, no
[Cleaner]attribute is needed. - Built-in infrastructures (SqlServer, PostgreSql, Sqlite via TestContainers) already implement their own cleaners.
Logging
DoggyDog now logs infrastructure lifecycle events during normal test execution โ not just on crash recovery. You will see log entries when an infrastructure is initialized, reset, or destroyed as part of a clean test run.
Configuration
DoggyDog can be configured via your testsettings.json file.
Disabling DoggyDog
To disable DoggyDog for the entire test project, set DisableWatchdog to true under the Environment section:
{
"Environment": {
"DisableWatchdog": true
}
}
All registry operation and doggydog start will be skip.
Manual Launch (debugging)
For debugging purposes, you can instruct the test suite to wait for DoggyDog to be launched manually instead of spawning it automatically. Set ManualLaunch to true under the Watchdog section:
{
"Watchdog": {
"ManualLaunch": true
}
}
When ManualLaunch is enabled, the environment will publish the required parameters as User Environment Variables. You can then start DoggyDog manually with the --from-env flag:
DoggyDog --from-env
This lets you attach a debugger to DoggyDog or inspect its behavior before the test campaign proceeds.
๐ How to Access Infrastructures in Tests
Once a test environment is configured, retrieve any registered infrastructure inside your tests via GetInfrastructure<T>():
// xUnit
public class MyIntegrationTests : NotoriousTest.XUnit.IntegrationTest<MyTestEnvironment>
{
public MyIntegrationTests(MyTestEnvironment environment) : base(environment) { }
[Fact]
public async Task ExampleTest()
{
var infra = CurrentEnvironment.GetInfrastructure<MyInfrastructure>();
Assert.NotNull(infra);
}
}
๐ Key Takeaways:
GetInfrastructure<T>()โ retrieves an infrastructure by type. Throws if not found.- No async needed โ infrastructure retrieval is synchronous.
- Tests remain clean and focused on logic instead of infrastructure handling.
โ Summary
| Concept | Description |
|---|---|
| Concept | Description |
| --------- | ------------- |
Infrastructure | Base class for all external dependencies (DB, API, queueโฆ). |
IInfrastructureExtension | Lifecycle hooks to add reusable behaviors to any infrastructure. |
SettingsExtension<T> | Loads typed settings from testsettings.json before Initialize(). |
OutputConfigurationExtension<T> | Publishes configuration entries for other infrastructures to consume. |
IConfigurationProducer | Contract for infrastructures that publish configuration entries. |
IConfigurationConsumer | Contract for infrastructures that receive the aggregated entries. |
Environment | Groups infrastructures, owns the DI container and lifecycle. |
ConfigureInfrastructureServices | Override to register custom services injected into infrastructure constructors. |
ITestLogger | Framework-native logging, injected via Infrastructure.Logger. |
| DoggyDog | Watchdog process that cleans up after unexpected test crashes. |
[Cleaner(typeof(T))] | Designates the crash-recovery handler for an infrastructure. |
GetInfrastructure<T>() | Retrieves a registered infrastructure inside a test. |
Now that you understand the fundamentals, check out:
- ๐ Supported Infrastructures โ See how to integrate SQL Server, TestContainers, and more.
- ๐ Examples โ Hands-on use cases with real-world setups.
- ๐๏ธ Architecture Guidelines โ Best practices for structuring your test setup.
๐ก Need help or have feedback? Join the community discussions or open an issue on GitHub.