✓
Home›Tutorials›.NET & ASP.NET Core›ASP.NET Core 10 Testing Tutorial: Verify Endpoints, Auth, and EF Core Paths
.NET & ASP.NET Core4 min read

ASP.NET Core 10 Testing Tutorial: Verify Endpoints, Auth, and EF Core Paths

Lesson 6: build repeatable ASP.NET Core 10 integration tests for endpoint behavior, authentication boundaries, and SQLite-backed data flows.

ASP.NET Core 10 Testing Tutorial: Verify Endpoints, Auth, and EF Core Paths

Lesson 6 of the .NET & ASP.NET Core learning track. Your API now validates input and returns consistent error responses. This lesson turns that behavior into repeatable tests that boot the application in memory, call real endpoints, cross authorization boundaries, and exercise an EF Core-backed path.

Prerequisite: complete Lesson 5: validation and consistent Problem Details. Keep the full .NET & ASP.NET Core learning track open as the roadmap.

Learning outcome

By the end, you can write an integration test that starts your real pipeline with WebApplicationFactory<Program>, asserts an HTTP contract, replaces an external dependency, and uses a SQLite in-memory database rather than pretending a relational provider behaves like an in-memory dictionary.

Why integration tests belong at API boundaries

Unit tests are excellent for focused rules. They do not prove route mapping, middleware order, authentication configuration, JSON serialization, or database registration work together. ASP.NET Core’s integration-testing guidance uses a test host and in-memory server; Microsoft.AspNetCore.Mvc.Testing supplies WebApplicationFactory to bootstrap the application. The same guidance recommends SQLite for in-memory relational testing over the EF Core InMemory provider when relational behavior matters.

Create the test project

dotnet new xunit -n Store.Api.Tests
dotnet add Store.Api.Tests reference Store.Api
dotnet add Store.Api.Tests package Microsoft.AspNetCore.Mvc.Testing
dotnet add Store.Api.Tests package Microsoft.EntityFrameworkCore.Sqlite
dotnet test

Expose the generated top-level Program class to the test assembly by adding public partial class Program { } at the bottom of the API’s Program.cs. This changes no runtime behavior; it gives the test a type to use as its entry point.

Test a public endpoint and validation contract

using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;

public sealed class ProductEndpointTests(WebApplicationFactory<Program> factory)
    : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task Create_with_missing_name_returns_validation_problem()
    {
        var client = factory.CreateClient();

        var response = await client.PostAsJsonAsync("/products", new { name = "" });

        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
        Assert.Equal("application/problem+json",
            response.Content.Headers.ContentType?.MediaType);
    }
}

Assert the public contract: status, content type, and stable fields that a client relies on. Do not make a test fragile by asserting every generated detail, timestamp, or framework-owned wording. The test above protects the agreement established in Lesson 5 rather than merely checking that “something failed.”

Test the authorization boundary

Use one test for the anonymous outcome and another that substitutes a test authentication handler for an authenticated identity. Do not mint production JWTs or call an external identity provider in this suite. Microsoft’s integration-test guidance demonstrates test authentication through a mock AuthenticationHandler<TOptions> configured in test services.

var authorizedFactory = factory.WithWebHostBuilder(builder =>
{
    builder.ConfigureTestServices(services =>
    {
        services.AddAuthentication("Test")
            .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", _ => { });
    });
});

var anonymous = factory.CreateClient(new WebApplicationFactoryClientOptions
{
    AllowAutoRedirect = false
});
Assert.Equal(HttpStatusCode.Unauthorized, (await anonymous.GetAsync("/orders")).StatusCode);

var authenticated = authorizedFactory.CreateClient();
authenticated.DefaultRequestHeaders.Authorization = new("Test");
Assert.Equal(HttpStatusCode.OK, (await authenticated.GetAsync("/orders")).StatusCode);

Implement TestAuthHandler in the test project so it returns a fixed claims principal only for the test scheme. Keep the test policy and claims realistic: an authenticated user and an administrator are different cases. This builds directly on Lesson 4’s authentication and authorization model.

Exercise a database-backed path

For a test-specific factory, remove the production DbContextOptions registration, open one SQLite connection with DataSource=:memory:, register it, and keep that connection open for the factory’s lifetime. Call Database.EnsureCreated(), seed only data needed by the scenario, then issue the HTTP request. This catches a missing migration mapping, query translation issue, or serialization boundary that a repository mock cannot see.

Common mistakes

  • Sharing mutable seeded data between tests and then depending on execution order.
  • Using EF Core InMemory for a test that depends on relational constraints or SQL translation.
  • Following an auth redirect and asserting the final page instead of the initial 401/403.
  • Testing framework internals instead of the API contract your caller consumes.
  • Calling real payment, email, or identity systems from deterministic integration tests.

Validation and review steps

  1. Run dotnet test repeatedly and confirm tests pass in any order.
  2. Break a route, authorization policy, and validation rule one at a time; make sure a focused test fails.
  3. Run the suite in CI with a separate integration-test job and publish test results.
  4. Keep a small smoke suite for deployments; do not confuse it with the full test suite.

Production guidance

Tests are a safety net, not an availability mechanism. Pair them with the health checks, logs, and dependency budgets used in production. Keep secrets out of test configuration, use isolated test databases for real-provider tests, and make fixtures disposable. Add contract tests where another team owns the dependency.

Next lesson

Lesson 7: Containerize the API with Docker. You will create a small, reproducible image, run it locally with configuration supplied at runtime, and keep development conveniences out of the production image.