Codebelt
Codebelt xUnit test infrastructure icon with lab flask and green check mark.

Unified Functional Testing for .NET

Extensions for xUnit API makes it more convenient to use either ApplicationTestFactory.Create<TEntryPoint>(...) for .NET applications or WebApplicationTestFactory.Create<TEntryPoint>(...) for ASP.NET Core applications; surpassing the official WebApplicationFactory<TEntryPoint> class in terms of flexibility and ease of use.

LIBRARIES
  • .NET
  • xUnit
  • Testing
  • TestHost
  • ASP.NET
  • WebApplicationFactory
  • ApplicationTestFactory
  • WebApplicationTestFactory

Functional testing should not be a web-only experience

Codebelt ecosystem card for application test factory support.Codebelt ecosystem card for application test factory support.

ASP.NET Core has long had a strong functional and integration testing story through WebApplicationFactory<TEntryPoint>. It gives test projects a supported way to bootstrap an ASP.NET Core application, run it on TestServer, create an HttpClient, and exercise the application through the same request pipeline that production code depends on.

That model is valuable because it reduces guesswork. Instead of rebuilding the application one service at a time inside the test project, the test references the real application entry point and verifies behavior through the running host.

However, modern .NET systems are rarely web-only.

A typical solution may include:

  • an ASP.NET Core API;
  • one or more worker services;
  • hosted background processors;
  • console applications;
  • Generic Host applications;
  • shared infrastructure registrations;
  • cross-cutting services such as logging, configuration, validation, messaging, persistence, and telemetry.

The official WebApplicationFactory<TEntryPoint> model is intentionally scoped to ASP.NET Core. That is the right boundary for the framework, but it leaves a consistency gap for teams that want the same fast, entry-point-driven testing experience across the rest of their .NET applications.

Extensions for xUnit API by Codebelt closes that gap.

The Codebelt way: one mental model for hosted .NET applications

The Codebelt testing model introduces a unified entry-point pattern for functional testing:

  • use ApplicationTestFactory.Create<TEntryPoint>(...) for console, worker, and Generic Host applications;
  • use WebApplicationTestFactory.Create<TEntryPoint>(...) for ASP.NET Core applications;
  • customize the host only where the scenario requires it;
  • assert through the real host, real dependency injection graph, real configuration, and, for web applications, the real ASP.NET Core pipeline;
  • dispose the returned test context directly, or let xUnit fixture lifecycle manage it.

That is the essential developer-experience improvement: the test starts from the application, not from a handcrafted reconstruction of the application.

When tests rebuild the host manually, they can drift from production behavior. When every application type has its own testing style, teams pay an unnecessary tax in boilerplate, conventions, and onboarding. Codebelt makes the testing surface more consistent: choose the entry point, apply test-only overrides, run the scenario, assert the result.

Less ceremony. Less duplicated host setup. Faster feedback.

What unified functional testing means

Functional testing, in this context, means validating observable application behavior through the application host.

It is broader than a narrow unit test, because it allows multiple components to work together. It is also more focused than a full end-to-end environment, because the application can still run in-process with test-specific dependencies, configuration, and infrastructure substitutes.

A functional test should answer questions such as:

  • Does the application start with the expected configuration?
  • Are required services registered and resolvable?
  • Does the worker register its handlers, processors, or hosted services?
  • Does the ASP.NET Core pipeline return the expected response?
  • Can test-only dependencies be substituted without rewriting the production startup path?
  • Does the application fail early when the host is misconfigured?

The goal is not to test every permutation. The goal is to verify the application contract at the right boundary with enough real infrastructure to catch meaningful integration mistakes.

The official baseline: WebApplicationFactory<TEntryPoint>

Microsoft’s ASP.NET Core testing guidance uses WebApplicationFactory<TEntryPoint> to create a TestServer for ASP.NET Core integration tests. The test project references the system under test, creates a test web host, creates a client, submits requests, and asserts the responses.

That model is well understood and remains the official ASP.NET Core baseline.

A typical ASP.NET Core test follows this shape:

public class BasicTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/health")]
    public async Task Endpoints_return_success(string url)
    {
        var client = _factory.CreateClient();

        var response = await client.GetAsync(url);

        response.EnsureSuccessStatusCode();
    }
}

This is a strong model for ASP.NET Core applications.

The limitation is scope. WebApplicationFactory<TEntryPoint> is web-specific. It does not give the same first-class entry-point testing model to a worker service, a console application, or a Generic Host application.

Codebelt keeps the good part of the model and expands the experience.

Testing a .NET application entry point

ApplicationTestFactory.Create<TEntryPoint>(...) bootstraps an existing .NET application entry point and returns an owned test context. The test can inspect the host, resolve services, read configuration, verify the environment, and assert behavior without recreating the application setup inside the test project.

using Codebelt.Extensions.Xunit.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace WorkerApp.Tests;

public sealed class WorkerFunctionalTests
{
    [Fact]
    public void Worker_bootstraps_required_services()
    {
        using var application = ApplicationTestFactory.Create<Program>();

        var processor = application.Host.Services.GetRequiredService<IInvoiceProcessor>();

        Assert.NotNull(processor);
        Assert.Equal("Development", application.Environment.EnvironmentName);
    }
}

The important detail is not the number of lines. It is the ownership model.

The test project does not need to reproduce Program.cs. It does not need a parallel host builder that slowly diverges from the application. It starts from the application entry point and verifies the resulting host.

That makes the test more trustworthy.

Testing an ASP.NET Core application entry point

For ASP.NET Core applications, WebApplicationTestFactory.Create<TEntryPoint>(...) applies the same idea to the web host. The factory bootstraps the existing ASP.NET Core entry point, uses TestServer, and gives the test access to the resulting host.

using Codebelt.Extensions.Xunit.Hosting.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace CatalogApi.Tests;

public sealed class CatalogFunctionalTests
{
    [Fact]
    public async Task Health_endpoint_returns_test_ready()
    {
        using var application = WebApplicationTestFactory.Create<Program>(builder =>
        {
            builder.ConfigureServices(services =>
            {
                services.AddSingleton(new CatalogStatus("test-ready"));
            });
        });

        using var client = application.Host.GetTestClient();

        var body = await client.GetStringAsync("/health");

        Assert.Equal("test-ready", body);
    }
}

This keeps the test close to the production application while still allowing test-specific substitutions.

That is the balance functional tests should aim for: use the real startup path by default, override only what the test scenario needs, and assert through the host boundary.

Why this is more developer-experience friendly

The Codebelt approach is more devex friendly because it optimizes for the work developers do repeatedly.

  1. The test starts from the real application

    The entry point is the source of truth. The test references the application and lets the application build its host.

    That reduces the risk of false confidence caused by a test host that does not match production startup.

  2. The pattern is consistent across application types

    With the official ASP.NET Core approach, web applications have a first-class factory pattern. Other hosted application types often require a custom testing convention.

    Codebelt provides a more uniform model:

    ApplicationTestFactory.Create<Program>();
    WebApplicationTestFactory.Create<Program>();
    

    The naming is predictable. The intent is obvious. The difference between web and non-web hosting is explicit without forcing teams into completely different testing styles.

  3. Less code is required for common scenarios

    Most functional tests do not need a custom fixture class, a hand-written host builder, or repeated service-provider setup. They need to start the app, apply a small override, execute the behavior, and assert the outcome.

    Codebelt’s factory-first design supports that directly.

  4. Fast feedback remains the default

    Functional tests are more expensive than isolated unit tests, so the test harness should not add unnecessary friction. A small, readable factory call makes it easier to add targeted coverage for the scenarios that matter: startup, dependency injection, configuration, endpoint behavior, and host-level wiring.

    Fast feedback is not only about execution time. It is also about how quickly a developer can understand, write, and maintain the test.

  5. Fixtures are available when sharing is needed

    Factory-based tests are ideal for focused scenarios. When multiple tests should share the same application context, Codebelt also provides fixture and base-class patterns that integrate with xUnit lifecycle conventions.

    That gives teams a clean path from simple tests to shared test contexts without changing the underlying mental model.

Use ApplicationTestFactory.Create<TEntryPoint>(...) when the system under test is a non-web hosted application, such as:

  • a worker service;
  • a console application using Generic Host;
  • a background-processing application;
  • a message consumer;
  • a scheduled job host;
  • an application where dependency injection, configuration, and hosted services are part of the behavior being verified.

Use WebApplicationTestFactory.Create<TEntryPoint>(...) when the system under test is an ASP.NET Core application, such as:

  • a REST API;
  • a Razor Pages application;
  • an MVC application;
  • a minimal API;
  • an application where the ASP.NET Core request pipeline should be exercised through TestServer.

Use the lower-level host and web-host test factories when the test should construct the host directly instead of bootstrapping an existing application entry point.

A practical rule

If the test is verifying application wiring, startup behavior, dependency injection, configuration, environment-specific behavior, or request-pipeline behavior, prefer an entry-point-driven functional test.

If the test is verifying a small algorithm, value object, parser, mapper, or domain rule, prefer a unit test.

Functional testing should complement unit testing. It should not replace it.

Tip

Unified functional testing becomes more valuable when applications are built with a consistent host model in the first place.

That is where Bootstrapper API by Codebelt complements the testing story. Bootstrapper provides a uniform and consistent way to bootstrap .NET applications using either the classic Program.cs plus Startup.cs pattern or the newer minimal hosting style. The important part is that this consistency is not limited to ASP.NET Core. The same idea applies across console applications, worker services, web applications, MVC, Razor Pages, and Web API projects.

In practice, Bootstrapper and Extensions for xUnit API support the same architectural direction:

  • build applications from a predictable entry point;
  • keep hosting conventions consistent across project types;
  • support both legacy and minimal bootstrapping models;
  • reduce application-specific ceremony;
  • make functional tests easier to write because the host shape is already familiar.

That pairing creates a cleaner developer experience. Bootstrapper helps standardize how applications are composed. Extensions for xUnit API helps standardize how those applications are verified.

Together, they make the Codebelt approach clear: .NET application development should feel unified whether the entry point is a console app, a worker, or an ASP.NET Core application.

A better default for application-level confidence

The official ASP.NET Core testing stack remains a proven baseline for web applications. Codebelt builds on the same principle and applies it more broadly: application-level tests should start from the application entry point and exercise the real host.

That makes functional testing feel the same whether the application is a web API, a worker, a console app, or a Generic Host process.

For teams building modern .NET systems, that consistency matters. It lowers the cost of writing meaningful tests, reduces duplicated setup, improves trust in the test harness, and gives developers faster feedback with less code.

Unified functional testing is not about adding another abstraction for its own sake. It is about making the right testing boundary easier to reach.

Sources