NUnit
ProtoTest.NUnit starts the host from a [SetUpFixture] and wraps each [ProtoTest] method in a ProtoTest context. The wrapper sits outside NUnit's setup and teardown, so the lifecycle spans [SetUp], the body and [TearDown]. The dotnet new prototest template writes this adapter by default.
Install
dotnet add package ProtoTest.NUnit
ProtoTest targets .NET 8, 9 and 10, and needs NUnit 4.6.1 or newer; the standard dotnet new nunit template pins an older version, so update NUnit first: dotnet add package NUnit --version 4.6.1. The dotnet new prototest template defaults to net10.0; pass --framework net8.0 or --framework net9.0 for an older runtime.
Register
ProtoTestAssembly carries [SetUpFixture] and owns [OneTimeSetUp] and [OneTimeTearDown]. Derive from it and configure the host:
using NUnit.Framework;
using ProtoTest.Core;
using ProtoTest.NUnit;
[SetUpFixture]
public sealed class Setup : ProtoTestAssembly
{
protected override void Configure(IProtoHostBuilder builder) =>
builder.AddApplication("Api", app => app
.AddRest(rest => rest.AddClient("Api")));
}
The test attribute is [ProtoTest]. It derives from NUnit's TestAttribute, so it replaces [Test]:
[TestFixture]
[Application("Api")]
public class OrderTests
{
[ProtoTest]
public async Task Orders_endpoint_responds()
{
using var response = await Proto.Context.Rest().GetAsync("/api/orders");
response.Should.HaveHttpStatus(HttpStatusCode.OK);
}
}
This is NUnit's own rule. A [SetUpFixture] outside any namespace applies to the whole assembly, while one inside a namespace applies only to that namespace and its children. If tests in another namespace cannot find the host, move the setup class to cover that namespace. The dotnet new prototest template keeps its Setup inside the project namespace by design, beside the tests it generates.
Assembly
├── [SetUpFixture] outside any namespace → every namespace below sees the host
│ ├── Orders.Tests → host available
│ └── Billing.Tests → host available
└── [SetUpFixture] inside namespace Orders.Tests → only that namespace sees it
├── Orders.Tests → host available
└── Billing.Tests → InvalidOperationException naming the setup class
Low-ceremony mode. Add [assembly: ProtoTestAutoWrap] and every plain [Test] runs through the same lifecycle. NUnit applies the nearest IWrapSetUpTearDown attribute (method, then fixture, then assembly), so a test that carries [ProtoTest] keeps its own wrapper and is never wrapped twice.
The context window
| is the host bar, [] is the context. Everything outside the bracket runs before the context exists:
|[SetUpFixture (host)]| [[SetUp | body | TearDown]]
Proto.Context works in [SetUp], the body and [TearDown]. It does not work in the fixture's [OneTimeSetUp], which builds the host.
What the adapter changes
| Item | What the adapter does |
|---|---|
| The host | ProtoTestAssembly already carries [SetUpFixture] and starts and stops the one host for the assembly. |
| The test attribute | [ProtoTest] replaces [Test]. It implements NUnit's IWrapSetUpTearDown, which is how the lifecycle wraps [SetUp], the body and [TearDown]. |
| The lifecycle | The wrapper resolves the method's attributes and skip conditions, starts the context, then runs NUnit's own test command. It maps the result NUnit recorded and completes the context in a finally. A setup failure rolls back and fails the test. |
| Scheduling | Both calls block on the host task (GetAwaiter().GetResult()), so NUnit needs a synchronizing context. |
| Cancellation | The adapter passes TestExecutionContext.CancellationToken, which [CancelAfter] cancels. The body reads it as Proto.Context.CancellationToken. |
| Outcomes | Passed to Passed, Failed to Failed, Skipped to Skipped, Inconclusive to Skipped, Warning to Partial. Anything else is Unknown. |
| Failure detail | A body exception that escapes the command is kept unwrapped and recorded with its real type, message and stack. When NUnit records the failure itself, the type is NUnit.{Label}, or NUnit.Failed when NUnit reports no label. NUnit exposes no exception type, so a runner-cancelled test reads as Failed. |
| Skips | The conditions run before anything else, not even [SetUp]. A skip reports an ignored result (ResultState.Ignored), and the reason is reported as given. |
| Attachments | TestContext.AddTestAttachment(path, description), using the attachment's description or its name. |
| Test names | NUnit's name for the case: the fully qualified method name, with the row's arguments for a parameterized test. |
| Auto-wrap | Optional. [assembly: ProtoTestAutoWrap] wraps every plain [Test], and the nearest-wrapper rule keeps an explicit [ProtoTest] in charge. |
Limits
IWrapSetUpTearDownis synchronous, so the host calls block. NUnit needs a synchronizing context.[SetUpFixture]scoping is NUnit's namespace rule. Keep tests in or under the namespace of the setup class.- A skipped test is reported only by NUnit. ProtoTest records nothing for it.
- NUnit's result carries no exception, so the adapter cannot tell a cancelled test from a failed one. A runner-cancelled test reads as
Failed. - Auto-wrap follows NUnit's nearest-wrapper rule. A fixture-level
IWrapSetUpTearDownattribute other than[ProtoTest]suppresses it for that fixture, exactly as it would suppress[ProtoTest]. - Parallel execution works because ProtoTest scopes its context per async flow. See Concurrency.
Learn more
- Test runners: the five adapters side by side.
- Skip conditions: the conditions every adapter evaluates.
- Lifecycle: the hooks and attributes around a test.