Your first test
This page takes a fresh test project to a passing test. It then breaks the test and reads the trace. The steps assume an ASP.NET Core application, Orders.Api, beside the tests; the tip below creates one. It uses NUnit. The other runners differ only in the setup class, covered in Test runners.
dotnet new install ProtoTest.Templates, then dotnet new prototest -n Orders creates an API and a suite for it that is already composed, traced and reported. Steps 1, 2, 3 and 6 below are ready to run, and --runner writes the suite for xUnit v2, xUnit v3, TUnit or MSTest instead. See Installation.
The six steps
- Project. A test project with the runner and integration packages.
- Host. One setup class that builds the host, the sink, and the application.
- Test. One passing test, one trace file under
TestResults/. - Assert. A shape on the body, still green.
- Break it. One wrong expectation, one failure message that names the fix.
- Trace. The
.prototracethat recorded all of it, read layer by layer below.
1. Create the project
dotnet new nunit -n Orders.Tests
cd Orders.Tests
dotnet add reference ../Orders.Api/Orders.Api.csproj
dotnet add package ProtoTest.NUnit
dotnet add package ProtoTest.Rest
dotnet add package ProtoTest.AspNetCore
dotnet add package ProtoTest.Reporting
ProtoTest.NUnit 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.
For a minimal-API application, make its entry point visible to the tests by adding this to Orders.Api:
public partial class Program;
2. Configure the host
One class per test project builds the host. With NUnit it is a [SetUpFixture]:
using NUnit.Framework;
using ProtoTest.AspNetCore;
using ProtoTest.Core;
using ProtoTest.NUnit;
using ProtoTest.Reporting;
using ProtoTest.Rest;
namespace Orders.Tests;
[SetUpFixture]
public sealed class Setup : ProtoTestAssembly
{
protected override void Configure(IProtoHostBuilder builder) =>
builder
.AddSink<HtmlReportSink>(sink => sink.OutputPath = "TestResults/report.html")
.AddApplication("Api", app => app
.AddAspNetCoreServer<Program>()
.AddRest(rest => rest
.AddClient("Api")
.AddCollector<RestCoverageCollector>()));
}
This describes one application, Api, that exposes REST. It runs your application in-process. No deployed environment and no port are needed. Point it at a real address in an environment by setting ProtoTest:Applications:Api:BaseUrl. The sink writes TestResults/report.html under the test project's output folder, next to the trace in step 6. The template names the report after the project (Shop.html); this page uses the fixed name report.html.
A [SetUpFixture] only covers its own namespace and the namespaces below it. Keep your tests in or under Orders.Tests.
3. Write a test
using System.Net;
using NUnit.Framework;
using ProtoTest.Core;
using ProtoTest.NUnit;
using ProtoTest.Rest;
namespace Orders.Tests;
[Application("Api")]
public sealed class OrderTests
{
[ProtoTest]
public async Task CreatingAnOrderReturnsIt()
{
using var response = await Proto.Context.Rest()
.Body(new { product = "notebook", quantity = 2 })
.PostAsync("/api/orders");
response.Should.HaveHttpStatus(HttpStatusCode.Created);
}
}
[ProtoTest]replaces NUnit's[Test]and wraps the test in a ProtoTest context.[Application("Api")]selects theApiapplication;Proto.Context.Rest()then uses its default REST client.Proto.Contextis available anywhere in the test: no base class, no injected parameter.
Run it with dotnet test. One test passes, and a trace file lands under TestResults/.
4. Assert on the response
A status code says little. Describe the parts of the body the behavior depends on:
using ProtoTest.Json;
response
.Should.HaveHttpStatus(HttpStatusCode.Created)
.Should.MatchShape(new
{
id = JsonValue.GreaterThan(0),
product = "notebook",
quantity = 2,
status = "pending"
});
The shape is partial. The matcher ignores properties you do not list and reports all mismatches at once with their JSON paths. See Shape matching.
5. Make it fail once
Change status = "pending" to "cancelled" and run the test again. The test fails with the request, the JSON path and both values in the message:
POST /api/orders - Shape mismatch failed with 1 error(s):
• [$.status]: Values did not match. (Expected: "cancelled", Actual: "pending")
The message names the fix. Change the expectation back, and the test passes. The Read the trace lesson walks a real failing trace the same way.
6. Read the trace
Tracing is on by default. Without ConfigureTracing the trace is written to TestResults/prototest-{runId}.prototrace under the test project's output folder. The sink from step 2 writes TestResults/report.html beside it.
To choose the trace path yourself, add one line to Setup:
builder.ConfigureTracing(trace => trace.OutputPath = "TestResults/orders.prototrace");
Run the tests, then:
- drop the trace file (
TestResults/orders.prototracewith the line above, otherwise the defaultTestResults/prototest-{runId}.prototrace) onto trace.prototest.dev to see every step of the test, the request and the shape comparison. The file is a binary archive, so open it in the viewer or print it withprototest summary <file.prototrace>(ProtoTrace); reading it as text shows nothing useful. - open
TestResults/report.htmlfor the endpoints the suite exercised. See Reporting.
One recorded journey reads like this. The walk below is the sample suite's project journey (ProjectsJourney.CreatingAProjectReturnsIt), which follows the same six steps against a real application:
- Runbefore the first test
What the run composed, and where the application ran.
Recorded operations (3)
- REST, GraphQL, ASP.NET Core, SQL, Data, Sheets, Playwright
- Northstar web on a loopback listener
- Messaging broker
- Setup500.5 ms
Six hooks and four attributes run before the body: clients, the database connection, and the tenant the test asked for.
Recorded operations (4)
- Setup
- Rest, GraphQL, loopback web, in-process Northstar, probe, messaging
- Open SqliteConnection
- Application, NorthstarTenant, SignedInAs, NorthstarMember
- Execution178.0 ms
One request, the application event it caused, and the two checks that decided the test.
Recorded operations (4)
- REST POST /api/v1/projects
- project.create
- Assert status 201 Created
- Assert response shape
- Teardown36.7 ms
Attributes and hooks reverse, four attachments publish, and owned resources release in order.
Recorded operations (3)
- Request, response, expected shape, scenario summary
- Cleanup TenantResponse
- Services, connection, consumer, broker, readiness, loopback
docs/static/lessons/l1-first-journey.prototrace. The viewer draws the same trace from that archive (download it and drop it on the viewer).The ConfigureTracing line above is optional. Without it the trace keeps its default name; everything else on this page stays the same.
Going further: turn setup into a capability
When every order test needs a signed-in customer, write the setup once as an attribute and compose it onto any test. Attributes shows the worked example: a Customer attribute, an authenticator that reads it, and the ordering and teardown rules that make the pair safe.
Limits
- One context per async flow. Starting a second test before completing the active one throws.
Proto.Contextoutside a test throws and names the alternatives (ProtoHost.FindTraceWriter(Activity?)off-flow,ProtoHost.CurrentHostfor run scope). - A skip starts nothing. A skipped test never creates a context. See Skip conditions.
- Names are unique per test. A name without a prefix is stored as
{testId}-{name}, and a duplicate attachment name throws. - Ids are configurable.
ConfigureTestIds(ids => ids.RunPrefix = 42)fixes the run prefix;SequenceDigitsdefaults to6and accepts 1 to 9. See Configuration.
Where to next
- Configuration: the host options, and how to run the same suite against a deployed environment.
- Troubleshooting: when the host, a client or a container does not come up.
- Foundation: how the lifecycle, context and attributes fit together.
- Observability: what your suite covered, and what it did not.
- Recipes: common journeys, ready to adapt.