Leveraging WebApplicationFactory with .NET Core Minimal APIs
Published onWhat are Minimal APIs?
Without getting too much into the weeds, the new .NET Core minimal APIs provide a succinct way to define your API using reduced files, dependencies, and boilerplate.
To learn more about them, check out the Minimal APIs Overview on Microsoft's documentation website or explore the tutorial for a more hands-on approach.
Love them or hate them, the minimal APIs are now the default when using dotnet new
.
Consequently, I'd encourage you to learn and engage with them-- you're likely to encounter them in the future.
The Concern
Notably, minimal APIs don't require an explicit Program
class declaration nor a Startup
.
For those familiar with WebApplicationFactory
, you'll remember that it takes a generic type argument TEntryPoint
.
In most cases I've seen, the type used for the entry point is either Startup
or Program
. Does that mean WebApplicationFactory
is busted? Fear not.
Today, we'll explore two options I'm aware of for testing your Minimal API.
InternalsVisibleTo
As a Means to See Program
Under the hood, Program
is generated implicitly with internal
accessibility. Knowing this, a simple workaround for making a Program
type accessible to your WebApplicationFactory
is simply by adding an InternalsVisibleTo
tag to your service's csproj file.
Your csproj might look like the following:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Farmer.Codes.MinimalApis.WebApplicationFactoryDemo.InternalsVisibleToTests" />
</ItemGroup>
</Project>
And your WebApplicationFactory<TEntry>
might look like:
using Microsoft.AspNetCore.Mvc.Testing;
namespace Farmer.Codes.MinimalApis.WebApplicationFactoryDemo.InternalsVisibleToTests;
internal class TestApplication : WebApplicationFactory<Program>
{
// You'd override methods as appropriate for your test needs here.
}
Caveats
This solution is referenced by the Microsoft documentation (see the tutorial link above), so it's reasonable to consider it an acceptable solution. However, exposing internals to your assembly might not be desirable for whatever reason. Your testing philosophy might be such that you should only be testing public contracts and one could argue whether or not this violates that. I'll avoid making a judgement call in this post.
Using an Alternative Class as a Proxy to your Assembly
Despite the common practice of using either Startup
or Program
as the TEntryPoint
argument in your application, it's not at all necessary to be one of those. In fact, Microsoft's documentation states:
The
WebApplicationFactory<TEntryPoint>
will find the entry point class ofTEntryPoint
assembly and initialize the application by callingIWebHostBuilder CreateWebHostBuilder(string [] args)
onTEntryPoint
.This constructor will infer the application content root path by searching for a
WebApplicationFactoryContentRootAttribute
on the assembly containing the functional tests with a key equal to theTEntryPoint
assemblyFullName
. In case an attribute with the right key can't be found,WebApplicationFactory<TEntryPoint>
will fall back to searching for a solution file (*.sln) and then appendingTEntryPoint
assembly name to the solution directory. The application root directory will be used to discover views and content files.The application assemblies will be loaded from the dependency context of the assembly containing
TEntryPoint
. This means that project dependencies of the assembly containingTEntryPoint
will be loaded as application assemblies.
What this means in practice is that we can actually specify a TEntryPoint
that is just a class belonging to the same assembly. For instance, you could create an empty AssemblyHint
class in the service project or reference another class present.
Your AssemblyHint
implementation might look like:
namespace Farmer.Codes.MinimalApis.WebApplicationFactoryDemo;
/// <summary>
/// This class is used as a hint to WebApplicationFactory{TEntryPoint}.
/// </summary>
public class AssemblyHint
{
}
Your WebApplicationFactory<TEntry>
might look like this:
using Microsoft.AspNetCore.Mvc.Testing;
namespace Farmer.Codes.MinimalApis.WebApplicationFactoryDemo.AssemblyHintTests;
public class TestApplication : WebApplicationFactory<AssemblyHint>
{
// You'd override methods as appropriate for your test needs here.
}
An example of using an existing class can be seen on Scott Hansleman's blog, where he casually demonstrates this same behavior explained above.
Conclusion
At the end of the day, both methods achieve the same thing. While I don't yet regularly take advantage of the minimal APIs, if I were to I think I'd take the second approach as I prefer the explicitness as well as avoiding unnecessarily exposing internal classes. Ultimately, you should choose the path you think best fits with your team, workflow, etc.
You can take a look at the full code from this post on GitHub.