SseAssertions.TUnit
0.5.0
Prefix Reserved
dotnet add package SseAssertions.TUnit --version 0.5.0
NuGet\Install-Package SseAssertions.TUnit -Version 0.5.0
<PackageReference Include="SseAssertions.TUnit" Version="0.5.0" />
<PackageVersion Include="SseAssertions.TUnit" Version="0.5.0" />
<PackageReference Include="SseAssertions.TUnit" />
paket add SseAssertions.TUnit --version 0.5.0
#r "nuget: SseAssertions.TUnit, 0.5.0"
#:package SseAssertions.TUnit@0.5.0
#addin nuget:?package=SseAssertions.TUnit&version=0.5.0
#tool nuget:?package=SseAssertions.TUnit&version=0.5.0
SseAssertions.TUnit
Scope: Test projects only. Not intended for production code.
TUnit-native Server-Sent Events (SSE) assertions for .NET. Fluent entry points over TUnit's Assert.That(...) pipeline for asserting on SSE event streams from HTTP response bodies, streams, and strings. AOT-compatible, trimmable, no runtime reflection in the assertion path.
Full documentation and roadmap: github.com/JohnVerheij/SseAssertions.TUnit
What ships
| Entry point | Receiver | Shape |
|---|---|---|
HasSseEvent(eventName) |
string |
Chain with WithData(predicate), AtLeast(n), AtMost(n), Exactly(n) |
HasSseEvent(eventName, minCount, cancellationToken) |
Stream |
Flat (Task<AssertionResult>); cancellation-bounded partial-buffer reads |
HasSseEvent(eventName, minCount, strictContentType, cancellationToken) |
HttpResponseMessage |
Flat; default-on Content-Type: text/event-stream validation |
IsServerSentEventsStream() |
string |
Lightweight discriminator. |
HasSseContentType(strict) |
HttpResponseMessage |
Header-only discriminator (no body read). strict: false (default) matches text/event-stream with any parameters; strict: true requires the bare media type with no parameters. |
HasFirstSseEvent(eventName) |
string |
Asserts the first parsed frame's event: name. Unlabeled frames match HasFirstSseEvent("message") per the WHATWG default. |
HasFirstSseEvent(eventName, cancellationToken) |
Stream |
Async; reads to end then asserts first frame. |
HasFirstSseEvent(eventName, strictContentType, cancellationToken) |
HttpResponseMessage |
Async; validates Content-Type (default-on) then asserts first frame. |
HasSseEventsInOrder(eventNames) |
string |
Chain with .WithStrictOrdering(). Default permits gaps between named events; strict mode requires contiguous appearance. |
HasSseEventsInOrder(eventNames, strictOrdering, cancellationToken) |
Stream |
Async flat form. strictOrdering: true requires contiguous. |
HasSseEventsInOrder(eventNames, strictOrdering, strictContentType, cancellationToken) |
HttpResponseMessage |
Async flat form with Content-Type validation (default-on). |
HasSseRetryDirective(millis) |
string |
Asserts a retry: directive is present. millis: null accepts any value; millis: <n> requires exact match. |
HasSseRetryDirective(millis, cancellationToken) |
Stream |
Async; same shape. |
HasSseRetryDirective(millis, strictContentType, cancellationToken) |
HttpResponseMessage |
Async; validates Content-Type (default-on) then inspects retry directives. |
HasSseRetryDirectiveFirst() |
string / Stream / HttpResponseMessage |
Asserts a retry: directive precedes the first non-empty data: field, checked at the wire-field level. An empty data: line carries no payload and does not count, so the standard ASP.NET Core control frame (event: retry + empty data: + retry: <ms>) passes. Stream / HttpResponseMessage forms take cancellationToken; HttpResponseMessage takes strictContentType (default-on). Matches the retry: directive field, not an event: retry named event: a stream emitting event: retry + data: with no retry: field fails. (v0.3.0+; empty-data: handling v0.4.1+) |
EndsCleanlyOnCancellation(cancellationToken) |
Stream / HttpResponseMessage |
Asserts a canceled read tears down via cooperative cancellation rather than a transport exception (IOException / HttpRequestException). HttpResponseMessage form takes strictContentType (default-on) and reads the body via ReadAsStreamAsync. (Stream v0.3.0+; HttpResponseMessage v0.4.0+) |
The chain on the string receiver composes WithData(Func<string, bool>) to narrow by data payload and AtLeast / AtMost / Exactly to terminate with a count assertion. The async receivers (Stream, HttpResponseMessage) use a flat-form entry point because composing an async body read with a synchronous chain is awkward in C#; the async-receiver chain is a candidate for a future release.
Install
dotnet add package SseAssertions.TUnit
The framework-agnostic SseAssertions core (defining the SseEvent public record, SseFrameParser, and SseFailureMessage factories) comes transitively.
Requirements: TUnit 1.50.0 or later, .NET 10. AOT-compatible, trimmable, no runtime reflection in the assertion path.
Quick start
[Test]
public async Task TickEndpoint_EmitsThreeTicks()
{
const string body = "event: tick\ndata: 1\n\nevent: tick\ndata: 2\n\nevent: tick\ndata: 3\n\n";
await Assert.That(body).HasSseEvent("tick").Exactly(3);
}
[Test]
public async Task NotificationEndpoint_PublishesAtLeastThreeTicks(CancellationToken ct)
{
using var response = await _client.GetAsync("/notifications/ticks?take=3", ct);
await Assert.That(response).HasSseEvent("tick", minCount: 3, cancellationToken: ct);
}
See the full README for the Wire-format syntax reference, Failure diagnostics catalog, Cookbook (5 patterns), Design notes, and Out-of-scope caveats.
License
MIT. Copyright (c) 2026 John Verheij.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- SseAssertions (>= 0.5.0)
- TUnit.Assertions (>= 1.48.6)
- TUnit.Core (>= 1.48.6)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
View the rendered release notes: https://github.com/JohnVerheij/SseAssertions.TUnit/releases/tag/v0.5.0
Minor release. Adds a value-pinning `HasSseRetryDirectiveFirst(int millis)` overload across all three receivers, so the leading `retry:` directive's position and value can be asserted in a single read of a forward-only response body. Purely additive; the `0.3.0` ApiCompat baseline is preserved.
### Added
- **`HasSseRetryDirectiveFirst(int millis)`** on the `string`, `Stream`, and `HttpResponseMessage` receivers asserts that a `retry:` directive precedes the first data-bearing event *and* that the leading directive's value equals `millis`. Position and value were previously assertable only separately (`HasSseRetryDirectiveFirst()` for position, `HasSseRetryDirective(millis)` for value), which a forward-only `HttpResponseMessage` body cannot satisfy across two calls; this overload pins both in a single read. On a value mismatch the failure names the expected and the observed leading `retry:` value.
### Changed
- The release workflow now publishes the matching `CHANGELOG.md` section as the GitHub release body (`body_path`), so release notes carry the full hand-written detail instead of GitHub's auto-generated commit summary.