(English is not my native language and i'm not using any AI to write this article)
(Links to code will be added so you can follow the whole process through the articles, do not hesitate to click links).
Never have you ever debug an integration test, find that it has fail, and unfortunatly press the red squared button to kill the debugging, skipping the whole teardown login?
Tell me you've never seen a integration test CI starting your test and for some reason, be stopped unexpectedly ? (Timeout, OOM, Job canceled).
That's why TestContainers has made Ryuk, a docker container alongside other that clean them after your tests.
It work absolutly wonderfull because it does not rely on the test process to clean your environment.
But i had one problem.
Docker was not available for the infrastructure i wanted to setup.
And my dev server was not always cleaned by the test.
A lot of orphan test message bus subscription where left alone.
Making the dev server a complete mess of useless queues with large random guid names. Databases suffered from the same problem.
That's why i have been working on a watchdog that handle not only docker containers, but any infrastructures that is setup in your tests.
And it is integrated in NotoriousTest, an integration test framework made to simplify the making of clean, reusable integration test.
DoggyDog has been made to work alongside NotoriousTest, all behaviour descripted here will be supported natively by NT without you doing anything.
But since we are here to talk about the watchdog, lets see how it works :
Doggydog, the nice and gentle dog that keeps tracks of your infrastructures.
DoggyDog is a self-contained executable, made with .NET 10.
It had the responsability to clean all your orphan infrastructures after the test process quit without success.
A well trained dog that wait for your test to crash
🐶 DoggyDog is well trained, he will sit gently before doing anything to your infrastructures. Don't worry, he will move only after the right signal is emitted.
You will need to provide a PID to the Watchdog.
He will wait until the process end.
After the process ended, he will look for a signal, more specificaly, a temporary signal file.
This file name is formatted with an "EnvironmentId" (nt-{EnvironmentId}.signal).
If he find this file, it means that the process has exited correctly and had the time to clean the environment.
ℹ Why DoggyDog do not use exit code to ensure the test process exit correctly or not ?
On Windows, the exit code is available for everyone as a public information.
On Linux, however, this information is available only for the parent process. Wich is not the case here since doggydog is finding the process with the PID.
For compatibility issues, i needed to find an alternative.🐶 Then, the DoggyDog will leave and return to his Doghouse until you need him for another guard duty.
But what if he doesn't find his signal file ?
If the file is not present, DoggyDog will start the clean process.
How DoggyDog takes care of the mess after dinner
Loading the test assembly as a plugin
While tests were running, DoggyDog loaded test assembly inside his default load context.
To do so, he takes a path to the test assembly and it's runtimes to load them inside his default load context.
You need to think of the test assembly as a plugin of DoggyDog.
You define classes with predefined interfaces and attributes, and DoggyDog will find them later to do his job.
DoggyDog takes runtimes as well to resolve ASP.NET Core assemblies and/or .NET assemblies that could be references inside the test projects. (e.g. Microsoft.* or System.* packages).
ℹ Why not using an Assembly Load Context ?
DoggyDog must be seen as a process "inside" the test process.
Later, we will see that DoggyDog use a sqlite registry to retrieve infrastructures types, and load them from the test assembly.
He will need to make comparaison between referenced types in Doggydog and types in the test project.
Comparaison between 2 instance of the same version but with different calling assembly will not result in equality. I didn't wanted to have to design DoggyDog around full reflection.
Runtimes paths are needed for two reasons :
- Because DoggyDog is self contained. You dont need to have .NET 10 installed on your machine to run DoggyDog (NotoriousTest's is trying to be as much compatible with .netstandard2.1, and when it's not possible, .NET 8). Therefore, he will not resolve framework types from the .net assembly.
- Because DoggyDog is a .NET executable, and does not reference ASP.NET Core. And i didn't wanted to make a direct reference to ASP.NET to handle assembly resolution.
Retrieving orphan infrastructures and their cleaner
The test process will populate an sqlite registry when he initialize an infrastructure with InfrastructureRegistryEntry:
- EnvironmentId : An ID that identifies a subset of infrastructures associated with a test campaign.
- InfrastructureId
- InfrastructureType
- Metadata and MetadataType : Necessary information to clean the infrastructure (such as connection strings, container name/id, subscription name, etc...)
For every infrastructures that have not been deleted from the test process, DoggyDog will :
- Load the infrastructure type.
- Retrieve the CleanerAttribute associated with the infrastructure class.
- Retrieve the IInfrastructureCleaner in the CleanerType property of the attribute.
- Instantiate the Cleaner as an IInfrastructureCleaner.
- Call the method CleanAfterCrash with the metadata object loaded from the registry, serialized as the MetadataType.
- Remove the infrastructure in the registry.
By adding on your infrastructures classes the cleaner attribute, and implementing an IInfrastructureCleaner. You can clean any infrastructure that you want, even if it's a docker container, a real database, a file.
Infrastructure events
DoggyDog is always watching the registry.
When the test project create an infrastructure in the registry, add a reset date, or delete an infrastructure, DoggyDog will see it and log into the console what happened.
The End
In conclusion, DoggyDog is a watchdog that wait for your test process to stop, look in a sqlite registry all infrastructures that have been created, and call their associated cleaner, while notifying what happened to them.
It was really fun to get through all this reflection stuff.
I had never had the chance to dive into it and making everything working makes me genuinly proud.
Do not hesitate to share your toughts about it, go through the repository , the docs and samples.
And to star the repo if you like it !
Have a great day !
🐶 Woof !




Top comments (0)