Testing top-level statements
Old-school .NET console applications with a Program.Main
method could easily be invoked from tests. You only needed to make the method and its containing class public. Then you could directly invoke it like any other method:
[Test]
public async Task InvokesNonTopLevelMain()
{
var result = await Main.Program.Main(Array.Empty<string>());
result.Should().Be(0);
}
That's not an option if you're using top-level statements because there's no Main
method in your code to make publicly accessible. In this case, your only option is to use reflection and invoke the Assembly.EntryPoint
method. You could do the same for console applications with Main
method as well:
[Test]
public void InvokesNonTopLevelEntryPoint()
{
var entryPoint = typeof(Main.Program).Assembly.EntryPoint!;
var result = entryPoint.Invoke(null, [Array.Empty<string>()]);
result.Should().Be(0);
}
Notice how I invoked the method synchronously, although it's the same console application as above, with an asynchronous Main
method. That's because the EntryPoint
method is synchronous despite that (it returns an int
, not a Task<int>
):
We can confirm this with a test:
[Test]
public void EntryPointIsNotAsync()
{
var entryPoint = typeof(Main.Program).Assembly.EntryPoint!;
entryPoint.ReturnType.Should().Be(typeof(int));
}
To access the entry point of an assembly, you need a reference to one of its types. If your console application with top-level statements has at least one public type, you can access the assembly using it:
[Test]
public void InvokesTopLevelSyncIntEntryPoint()
{
var entryPoint = typeof(SyncInt.AnyClass).Assembly.EntryPoint!;
var result = entryPoint.Invoke(null, [Array.Empty<string>()]);
result.Should().Be(0);
}
If your console application code consists only of top-level statements and nested classes (i.e., all code is in the Program.cs
file), then your only option is to make the generated Program
class with that code public. To do this, add the following to the very bottom of your Program.cs
file:
public partial class Program { }
This will make the Program
class public and accessible via reflection in your test class. Keep in mind though that this Program
class is not in any namespace:
[Test]
public void InvokesTopLevelProgramOnlyEntryPoint()
{
var entryPoint = typeof(Program).Assembly.EntryPoint!;
var result = entryPoint.Invoke(null, [Array.Empty<string>()]);
result.Should().Be(0);
}
You can find a sample solution in my GitHub repository. It contains all the test cases from this post and more.
While top-level statements of a project don't often need to be tested, it's good to know that they can be if a need arises. For example, you might want to add a couple of integration tests for a project to make sure that all the services are registered correctly with dependency injection and can be instantiated when the application starts up.