Skip to content

Writing a test

A Hardened test boots the real application and hands the test method whatever it asks for. A service, a mock, a request client and a generated API client are all parameters.

csharp
public class TodoTests {

    [HardenedTest]
    public async Task CreateTodo_AnswersCreated(TodosClient client, [Mock] ITodoStore store) {
        store.Add("ship it").Returns(new Todo(7, "ship it", false));

        var created = await client.Todos.PostAsync(new ClientModels.NewTodo { Title = "ship it" })
            .Returns<Created<ClientModels.Todo>>();

        Assert.Equal("/todos/7", created.Location);
    }
}

Three things happened before the method body ran.

  • client is the Kiota client generated from the application's own OpenAPI document. It sends through an HttpClient whose handler runs the application's pipeline in-process. See Typed clients.
  • store is an NSubstitute mock, from the DependencyModules.NSubstitute package the test project references, registered in the container the handler resolves from. The handler behind POST /todos used it. See Substituting services.
  • Returns<Created<ClientModels.Todo>>() checked that the call answered 201 and handed back the body and the Location header. See Asserting a response.

Nothing in the test names a port, a host or a serializer. dotnet new hardened-web writes a test project in this shape, and ClientModels is the alias that project declares for the client's model namespace.

What a test boots

[HardenedTest] builds the application the way a host would. The module graph is applied, configuration is resolved, startup services run, and each parameter of the test method is resolved from the provider. Any service the application registers is a parameter the test can ask for:

csharp
public class MathServiceTests {

    [HardenedTest]
    public void AddsValues(IMathService<int> mathService) {
        Assert.Equal(6, mathService.Add(1, 2, 3));
    }
}

The application is built for each test and disposed when the test ends.

Setting up a project

The test project references the testing package and a runner package:

xml
<ItemGroup>
    <PackageReference Include="Hardened.Shared.Testing" />
    <PackageReference Include="Hardened.Shared.Testing.xUnit" />
    <PackageReference Include="DependencyModules.NSubstitute" />
    <PackageReference Include="xunit.v3" />
    <PackageReference Include="xunit.runner.visualstudio" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" />
</ItemGroup>

[HardenedTest] comes from the runner package:

RunnerPackage[HardenedTest] is
xUnit v3Hardened.Shared.Testing.xUnita FactAttribute. dotnet test, the IDE runner and every xUnit assertion work unchanged
NUnitHardened.Shared.Testing.NUnitNUnit's test attribute, so the NUnit adapter discovers it

The xUnit package builds on xUnit v3. A project on xunit 2.x fails to compile with CS0433 on Assert.

An assembly attribute names the module under test:

csharp
// Bootstrap.cs
using Hardened.Shared.Testing.Attributes;

[assembly: HardenedTestEntryPoint(typeof(TodosLibrary))]

On the assembly it covers every test. On a class or a method it names a different module for those tests.

A web application adds [assembly: WebTesting] from Hardened.Web.Testing; see Sending requests.

Parameters

ParameterWhat arrives
Any registered serviceThe application's own registration, resolved from the test's container
[Mock] TA mock of T from the library the test project names, NSubstitute, Moq or FakeItEasy, registered over the application's registration. Substituting services
ITestContextNamed steps, a retry engine, a logger and the test's cancellation token. Steps and retries
ITestWebAppSends requests through the pipeline. Sending requests
A client typeA Kiota client, a Refit interface or any class taking one HttpClient, built over the pipeline. Typed clients
A trigger façadeApplication.Queues, .Topics, .Timers, .Changes, .Streams, .Blobs, .Invocations, generated from the handlers. Triggers and Testing AWS handlers

A parameter nothing can supply fails the test.

Environments in tests

The environment is named test unless the test says otherwise:

csharp
[HardenedTest]
[EnvironmentName("production")]
[EnvironmentValue("FEATURE_X", "on")]
public void UsesTheProductionSender(IEmailSender sender) {
    Assert.IsType<SmtpEmailSender>(sender);
}

[EnvironmentName] changes the name that [IfEnvironment] and environment-scoped configuration amenders are evaluated against. [EnvironmentValue] sets a variable for the test alone, without touching the process. Both are valid on a method, a class or the assembly, and the narrowest wins.

The environment is registered before the modules are applied, so a registration gated on the name is decided against the test's name rather than a process default.

Where the tests point

The template's test project references the library and not the host. ITestWebApp and the client parameters drive the pipeline the library declares, so the same tests hold whether the host is Kestrel, ASP.NET Core or Lambda. A test that needs the host names one; see Test hosts.

Next

Released under the MIT License.