Testing top-level statements

May 17th 2024 Unit Testing .NET

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>):

Synchronous entry point

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.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License