Writing a Test-Oriented Program Main
I propose we change our boilerplate Program.Main to something test-oriented, at least until C# 9 top-level programs feature is released. The reason is simple: your run of the mill static void Main
is almost never backed by tests, and therefore is a liability - a chink in the armor for something bad to take up residence.
Before going any further let’s take a look at a Program.Main generated with dotnet new webapi
.
How do we write a test for this? If we try to call Program.Main the process will wait for incoming connections within Run and our test will hang. The logical thing would be to return a mock from CreateHostBuilder, but it’s static so we cannot override it. Basically there is no seam we can use to inject a test double because the boilerplate code uses static functions.
You may say “Why write a test for that, it’s template code?”. There are many reasons why tests are appropriate here. How does Program respond to a range of command-line arguments? And what about six months down the road when Program has been modified to meet new requirements? At some point we are going to need coverage there.
For whatever reason it seems widely accepted in our industry that certain things don’t get tested. There are certainly cases that do not warrant tests, for example generated code (e.g., database migrations). But if you ask me the thing that is responsible for running my API controllers DEFINITELY needs to be tested. Let’s not let static void Main
get in our way.
We need to be able to create seams in Program, which also means we need to be able to derive from Program. Let’s see how that would look.
Let’s unpack this bit by bit to understand the benefits of each change. The first and most trivial difference is the order of Main and CreateHostBuilder: we now see them ordered alphabetically. More on that in another post, but the obvious benefit is a well-understood order - as opposed to a hard-to-keep-current dependence order.
The next thing we notice is that Program.Main constructs an instance of Program in order to call Run, and it uses reflection to call the constructor. So let’s unpack both of those. By constructing an instance of Program we take our first step away from the static method challenge. The benefit here is that a derived class (e.g., MockProgram) can provide method overrides and thus we have created a seam for injecting a mock IWebHost.
But why use reflection to create the instance? Well, if we do not use reflection then we are always going to get an instance of Program, rather than a derived type (e.g., MockProgram). Take a moment and think through all the scenarios/options - you’ll see that reflection is the only option that gives us a seam to create a derived Program. So the benefit is that we get the polymorphism we need to inject mocks where needed. Note that in order to pull this off we define a ProgramType property that a derived type can set to its own type, thus ensuring that the reflection-based constructor creates an instance of itself.
That’s really the only changes that matter, the rest are simple method-refactors. The real value is in the reflection-based construction.
With this new Program design, you can easily have full coverage on whatever custom stuff gets added to it - and all the benefits that come with full coverage.