Warning
Beyond this point may not be good for your health. The below shows a solution to a problem I hope you never experience. Enter under own risk!
Introduction
There will be times when you want to run a test of some variety locally in debug mode but not on a build server. I won’t get into the why’s now and people will say “but this should never happen” and on the whole I agree. But sometimes when you’re working with legacy codebases sometimes needs must and in this “Quick Tip” I’m going to show how using one of the built in attributes in the .NET code base can help.
Setting the Scene
So imagine the scenario; you have a unit test which only runs locally. This could be due to infrastruction limitations or some other reason but as part of your local build it catches some things and needs to run. The what isn’t important at the moment nor the why but how can we do this with minimal distruption to the code is the subject of this post.
Simple Solution
To achieve this you know you will need to use compiler directives and the DEBUG
symbol. This is perfectly fine and allows for such a thing.
#if DEBUG
[Fact]
#endif
public void DebugOnlyTest()
{
// Arrange
// Act
// Assert
}
You start off wrapping the FactAttribute value from xUnit in an if statement. This will work. The attribute will only mark the test to be run during “Debug” Solution configuration.
One test down and it works fine. However another item comes up and before you know it the “copy and paste” development has kicked in and your test suite is starting to be littered with debug if statements. This has to stop.
A Using Level Solution
The above is not a good solution for more than one test and if you’re in a code base with one exception then you will likely find more. So how do we address this and still make the code readable?
To answer this we can use a technique called “using alias directive” where you can create an alias for a namespace or a type.
More info can be found https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-directive
So we can create an alias for the xUnit FactAttribute but we also need to define a replacement for it. You can create your own attribute which can be swapped in when it’s “Release” however this means you are creating more code which isn’t required but will need to be maintained, explained and will look messy.
This is where part of the framework comes in.
As part of the .NET framework, and it’s been around since .NET 2.0 days, we can use the CompilerGeneratedAttribute
. This attribute indicates what has been generated and not user created. Now you could argue that there is no “generation” going on however as it is a self contained attribute being used by the compiler it kinda fits the bill. If anyone can point out a more appropriate attribute then please get in contact!
#if !DEBUG
using FactAttribute = System.Runtime.CompilerServices.CompilerGeneratedAttribute;
#endif
So what we do is remove the compiler directive conditional statement at the test level and add it around the alias at the top of the individual files. The limitation of this is all the tests which are marked as Facts in the current file will all be ignored so choose wisely.
The unit test can now go back to being “clean”.
[Fact]
public void DebugOnlyTest()
{
// Arrange
// Act
// Assert
}
When it is now built and the tests run locally they will be discovered and when run with a Release configuration on a build server they will be ignored.
Conclusion
In this post I have shown a quick tip of working with legacy code bases to remove noise from the code but allow for tests to be run locally but not on the build server. Another potential usage could be if you have an integration test which requires an active database connection for example.
If an integration test does require a database connection I woudld strongly recommend investing in your testing infrastructure to allow for creating a docker container with a db instance in, run the tests and then rip it down. If running a container is not possible then mocking the smallest amount of code possible to get it to work is another solution. You can run in memory versions of databases however I have heard and seen some very bad stories about doing that, as they can behave differently under different loads, so would strongly advise against this solution.
Again, I would like to point out this is not an ideal scenario to be in. I would strongly recommand against doing what I have suggested. However as you work through your career and see different issues and scenarios some times it just needs to be done and hopefully as a stop gap.
If you’ve found this post useful and it’s been your last hope to solving a problem then I wish you well and please contact me on Twitter @WestDiscGolf to let me know how you got into this situation.