{"id":50784958,"url":"https://github.com/johnverheij/grpcassertions.tunit","last_synced_at":"2026-06-12T07:02:10.527Z","repository":{"id":361894381,"uuid":"1256374907","full_name":"JohnVerheij/GrpcAssertions.TUnit","owner":"JohnVerheij","description":"TUnit-native gRPC assertions for .NET tests. Fluent assertions on gRPC call outcomes, including RpcException and status codes. AOT-compatible, no runtime reflection.","archived":false,"fork":false,"pushed_at":"2026-06-11T19:30:55.000Z","size":120,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-11T21:13:19.528Z","etag":null,"topics":["aot","assertions","dotnet","grpc","rpc","testing","tunit"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JohnVerheij.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-01T18:06:31.000Z","updated_at":"2026-06-11T19:30:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/JohnVerheij/GrpcAssertions.TUnit","commit_stats":null,"previous_names":["johnverheij/grpcassertions.tunit"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/JohnVerheij/GrpcAssertions.TUnit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohnVerheij%2FGrpcAssertions.TUnit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohnVerheij%2FGrpcAssertions.TUnit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohnVerheij%2FGrpcAssertions.TUnit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohnVerheij%2FGrpcAssertions.TUnit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JohnVerheij","download_url":"https://codeload.github.com/JohnVerheij/GrpcAssertions.TUnit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohnVerheij%2FGrpcAssertions.TUnit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34232790,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-12T02:00:06.859Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aot","assertions","dotnet","grpc","rpc","testing","tunit"],"created_at":"2026-06-12T07:02:09.598Z","updated_at":"2026-06-12T07:02:10.521Z","avatar_url":"https://github.com/JohnVerheij.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# GrpcAssertions.TUnit\n\n[![CI](https://github.com/JohnVerheij/GrpcAssertions.TUnit/actions/workflows/ci.yml/badge.svg)](https://github.com/JohnVerheij/GrpcAssertions.TUnit/actions/workflows/ci.yml)\n[![CodeQL](https://github.com/JohnVerheij/GrpcAssertions.TUnit/actions/workflows/codeql.yml/badge.svg)](https://github.com/JohnVerheij/GrpcAssertions.TUnit/actions/workflows/codeql.yml)\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/JohnVerheij/GrpcAssertions.TUnit/badge)](https://scorecard.dev/viewer/?uri=github.com/JohnVerheij/GrpcAssertions.TUnit)\n[![codecov](https://codecov.io/gh/JohnVerheij/GrpcAssertions.TUnit/branch/main/graph/badge.svg)](https://codecov.io/gh/JohnVerheij/GrpcAssertions.TUnit)\n[![NuGet](https://img.shields.io/nuget/v/GrpcAssertions.TUnit.svg)](https://www.nuget.org/packages/GrpcAssertions.TUnit/)\n[![Downloads](https://img.shields.io/nuget/dt/GrpcAssertions.TUnit.svg)](https://www.nuget.org/packages/GrpcAssertions.TUnit/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4.svg)](https://dotnet.microsoft.com/download/dotnet/10.0)\n\nTUnit-native gRPC assertions for .NET tests. Fluent entry points over TUnit's `Assert.That(...)` pipeline for asserting on gRPC call outcomes, with a framework-agnostic core (`GrpcAssertions`) that a future xUnit, NUnit, or MSTest adapter can reuse. AOT-compatible, trimmable, no runtime reflection in the assertion path.\n\n\u003e **Scope:** Test projects only. Not intended for production code.\n\n---\n\n## Table of contents\n\n- [Why this package](#why-this-package)\n- [Install](#install)\n- [Package layout](#package-layout)\n- [Namespaces (and a `GlobalUsings.cs` recommendation)](#namespaces-and-a-globalusingscs-recommendation)\n- [Quick start](#quick-start)\n- [Entry points](#entry-points)\n- [Failure diagnostics](#failure-diagnostics)\n- [Cookbook: common patterns](#cookbook-common-patterns)\n  - [Replacing hand-rolled `AsyncUnaryCall\u003cT\u003e` factories](#replacing-hand-rolled-asyncunarycallt-factories)\n  - [When *not* to use `ThrowsGrpcException`](#when-not-to-use-throwsgrpcexception)\n  - [Await the call against a generated client](#await-the-call-against-a-generated-client)\n- [Design notes](#design-notes)\n- [Stability intent (pre-1.0)](#stability-intent-pre-10)\n- [Roadmap](#roadmap)\n- [Family compatibility](#family-compatibility)\n- [Pair with](#pair-with)\n- [Contributing](#contributing)\n- [License](#license)\n\n---\n\n## Why this package\n\ngRPC failures surface as a single `RpcException` carrying a `Status` (a `StatusCode` plus a detail string). Asserting on that with raw `try`/`catch` plus `Assert.That(ex.StatusCode).IsEqualTo(...)` is verbose and easy to get subtly wrong: forgetting to assert that an exception was thrown at all, or matching the wrong status. Typical hand-rolled code:\n\n```csharp\nvar ex = await Assert.That(() =\u003e client.GetOrderAsync(request)).Throws\u003cRpcException\u003e();\nawait Assert.That(ex!.StatusCode).IsEqualTo(StatusCode.Unavailable);\nawait Assert.That(ex.Status.Detail).Contains(\"connection refused\");\n```\n\nThis library collapses that to one chain, and ships the `GrpcCallBuilder` test-double helper that removes the five-parameter `AsyncUnaryCall\u003cT\u003e` constructor every hand-rolled gRPC fake repeats.\n\n## Install\n\n```bash\ndotnet add package GrpcAssertions.TUnit\n```\n\n**Requirements:** TUnit 1.53.0 or later, .NET 10. The framework-agnostic `GrpcAssertions` core and its single `Grpc.Core.Api` dependency come transitively. The package is AOT-compatible, trimmable, and uses no runtime reflection in the assertion path.\n\n## Package layout\n\nThis repo ships **two** NuGet packages:\n\n| Package | Purpose | Depends on |\n|---|---|---|\n| [`GrpcAssertions`](https://www.nuget.org/packages/GrpcAssertions/) | Framework-agnostic core: `GrpcCallBuilder` test-double builders, `GrpcOutcomeRendering` failure-message formatting, and `GrpcExceptions` predicates | `Grpc.Core.Api` |\n| [`GrpcAssertions.TUnit`](https://www.nuget.org/packages/GrpcAssertions.TUnit/) | TUnit `Assert.That(...)` entry points: `ThrowsGrpcException`, the `StatusCode` shorthands, detail refinements, and `DoesNotThrowGrpcException`. **Most users want this one.** | `GrpcAssertions` + `TUnit.Assertions` + `TUnit.Core` |\n\nYou install `GrpcAssertions.TUnit`; `GrpcAssertions` and `Grpc.Core.Api` come transitively. Adapters for other test frameworks (NUnit, xUnit, MSTest) are *not* shipped today: they would reuse the `GrpcAssertions` core. Open a feature request if you need one.\n\n## Namespaces (and a `GlobalUsings.cs` recommendation)\n\nThe two packages place types in namespaces with deliberately-different scopes:\n\n| Type / member | Namespace | Auto-imported? |\n|---|---|---|\n| `ThrowsGrpcException()`, `DoesNotThrowGrpcException()`, `IsRpcException()` and the `IsUnavailable()` / `WithDetail()` chain | `TUnit.Assertions.Extensions` | **Yes**: TUnit auto-imports |\n| `GrpcExceptionAssertion`, `GrpcDoesNotThrowAssertion\u003cT\u003e` (the assertion classes behind the chain) | `GrpcAssertions.TUnit` | **No**: rarely referenced directly |\n| `GrpcCallBuilder`, `GrpcExceptions`, `GrpcOutcomeRendering` (test-double builder, predicates, rendering) | `GrpcAssertions` | **No**: needed at the call site; recommended for `GlobalUsings.cs` |\n| `RpcException`, `StatusCode`, `Status`, `Metadata` (the gRPC types) | `Grpc.Core` | **No**: needed at the call site; recommended for `GlobalUsings.cs` |\n\n**Recommended:** put the two non-auto-imported namespaces into a single `GlobalUsings.cs` in your test project so every test file sees them without ceremony:\n\n```csharp\nglobal using Grpc.Core;        // StatusCode, RpcException, Status, Metadata\nglobal using GrpcAssertions;   // GrpcCallBuilder, GrpcExceptions, GrpcOutcomeRendering\n```\n\n## Quick start\n\n```csharp\n// Assert a call faults with a specific status and detail, in one chain:\nawait Assert.That(() =\u003e client.GetOrderAsync(request, ct))\n    .ThrowsGrpcException(StatusCode.Unavailable)\n    .WithDetailContaining(\"connection refused\", StringComparison.Ordinal);\n\n// Status shorthands read fluently:\nawait Assert.That(() =\u003e client.GetServerInfoAsync(request, ct))\n    .ThrowsGrpcException()\n    .IsUnimplemented();\n\n// Assert a benign error is swallowed and the call completes:\nawait Assert.That(() =\u003e client.CancelOrderAsync(request, ct))\n    .DoesNotThrowGrpcException();\n\n// Build AsyncUnaryCall\u003cT\u003e test doubles without the five-parameter constructor:\nvar ok = GrpcCallBuilder.Success(new OrderReply());\nvar bad = GrpcCallBuilder.Faulted\u003cOrderReply\u003e(StatusCode.NotFound, \"no such order\");\n```\n\n## Entry points\n\nDelegate assertions, on `Assert.That(() =\u003e client.Method(...))` (auto-imported from `TUnit.Assertions.Extensions`):\n\n| Entry point | Behavior |\n|---|---|\n| `ThrowsGrpcException()` | Asserts the call throws a gRPC `RpcException` of any status. Returns a chain. |\n| `ThrowsGrpcException(StatusCode expected)` | Asserts the call throws an `RpcException` with the given status. Returns a chain. |\n| `DoesNotThrowGrpcException()` | Asserts the call completes without throwing an `RpcException`. |\n\nChain off `ThrowsGrpcException()` to refine:\n\n| Chain method | Behavior |\n|---|---|\n| `IsOk()`, `IsCancelled()`, `IsInvalidArgument()`, `IsDeadlineExceeded()`, `IsNotFound()`, `IsAlreadyExists()`, `IsPermissionDenied()`, `IsResourceExhausted()`, `IsFailedPrecondition()`, `IsAborted()`, `IsUnimplemented()`, `IsInternal()`, `IsUnavailable()`, `IsUnauthenticated()` | Assert the status equals the corresponding `StatusCode`. |\n| `WithDetail(string)` | Assert `Status.Detail` exactly equals the string (ordinal). |\n| `WithDetailContaining(string, StringComparison)` | Assert `Status.Detail` contains the substring using the given comparison. |\n| `WithTrailer(string key, string value)` *(v0.2.0+)* | Assert the exception's `Trailers` contain a text entry at `key` equal to `value` (ordinal). Keys match case-insensitively (gRPC lowercases keys). |\n| `WithTrailer(string key, ReadOnlySpan\u003cbyte\u003e value)` *(v0.2.0+)* | Assert the exception's `Trailers` contain a binary (`-bin`) entry at `key` whose bytes equal `value`. A `byte[]` converts implicitly. |\n\nException discriminator, on a caught `Exception`:\n\n| Entry point | Behavior |\n|---|---|\n| `IsRpcException()` | Asserts the exception is a gRPC `RpcException`. The failure message names the actual exception type. |\n\nFramework-agnostic core (`GrpcAssertions` namespace), for test doubles and non-TUnit consumers:\n\n| Core API | Behavior |\n|---|---|\n| `GrpcCallBuilder.Success\u003cT\u003e(T response)` / `Success\u003cT\u003e(T, Metadata?, Metadata?)` *(v0.2.0+)* | Builds a successful `AsyncUnaryCall\u003cT\u003e` (response, optional response headers and trailers, terminal `OK`). The trailers accessor returns a stable instance. |\n| `GrpcCallBuilder.Faulted\u003cT\u003e(RpcException)` / `Faulted\u003cT\u003e(StatusCode, string?)` / `Faulted\u003cT\u003e(StatusCode, string?, Metadata)` *(v0.2.0+)* | Builds a faulted `AsyncUnaryCall\u003cT\u003e` surfacing the exception's status and trailers. |\n| `GrpcExceptions.IsRpcException(Exception?)` | `true` when the argument is a gRPC `RpcException`; `false` for `null` or any other type. |\n| `GrpcOutcomeRendering.Describe(RpcException)` | Renders `RpcException with StatusCode X, Detail \"...\"` for failure messages. |\n\n## Failure diagnostics\n\nEvery failed assertion renders the actual gRPC outcome alongside the expectation. A status mismatch:\n\n```text\nExpected the gRPC call to throw an RpcException with StatusCode Unavailable\nbut it threw RpcException with StatusCode Internal, Detail \"Unhandled exception in pipeline\"\n```\n\nA call that should have thrown but completed:\n\n```text\nExpected the gRPC call to throw an RpcException\nbut no exception was thrown\n```\n\nA `DoesNotThrowGrpcException()` that faulted:\n\n```text\nExpected the gRPC call not to throw an RpcException\nbut it threw RpcException with StatusCode Unavailable, Detail \"connection refused\"\n```\n\n`Status.Detail` is truncated at `GrpcOutcomeRendering.MaxDetailLength` (200 characters) with a horizontal-ellipsis suffix, so a verbose server detail does not flood the test output.\n\n## Cookbook: common patterns\n\n### Replacing hand-rolled `AsyncUnaryCall\u003cT\u003e` factories\n\nThe single highest-value use of `GrpcCallBuilder` is deleting the fake-client constructor boilerplate. A generated gRPC client method returns `AsyncUnaryCall\u003cT\u003e`, and its public constructor takes five arguments: the response task, a response-headers task, a status accessor, a trailers accessor, and a dispose callback. Every hand-rolled client fake repeats that shape for every method.\n\nBefore, a fake that returns a canned response or faults on demand:\n\n```csharp\npublic sealed class FakeGreeterClient : Greeter.GreeterClient\n{\n    private readonly bool _fail;\n    private readonly MyResponse _reply;\n\n    public FakeGreeterClient(MyResponse reply, bool fail = false) =\u003e (_reply, _fail) = (reply, fail);\n\n    public override AsyncUnaryCall\u003cMyResponse\u003e SayHelloAsync(MyRequest request, CallOptions options)\n    {\n        if (_fail)\n        {\n            var ex = new RpcException(new Status(StatusCode.Unavailable, \"server down\"));\n            return new AsyncUnaryCall\u003cMyResponse\u003e(\n                Task.FromException\u003cMyResponse\u003e(ex),\n                Task.FromResult(new Metadata()),\n                () =\u003e ex.Status,\n                () =\u003e ex.Trailers,\n                () =\u003e { });\n        }\n\n        return new AsyncUnaryCall\u003cMyResponse\u003e(\n            Task.FromResult(_reply),\n            Task.FromResult(new Metadata()),\n            () =\u003e new Status(StatusCode.OK, string.Empty),\n            () =\u003e new Metadata(),\n            () =\u003e { });\n    }\n}\n```\n\nAfter:\n\n```csharp\npublic sealed class FakeGreeterClient : Greeter.GreeterClient\n{\n    private readonly bool _fail;\n    private readonly MyResponse _reply;\n\n    public FakeGreeterClient(MyResponse reply, bool fail = false) =\u003e (_reply, _fail) = (reply, fail);\n\n    public override AsyncUnaryCall\u003cMyResponse\u003e SayHelloAsync(MyRequest request, CallOptions options)\n        =\u003e _fail ? GrpcCallBuilder.Faulted\u003cMyResponse\u003e(StatusCode.Unavailable, \"server down\")\n                 : GrpcCallBuilder.Success(_reply);\n}\n```\n\n`Success\u003cT\u003e(T)` infers `T` from the response argument, so `GrpcCallBuilder.Success(_reply)` needs no type argument. `Faulted\u003cT\u003e(RpcException)` and `Faulted\u003cT\u003e(StatusCode, string?)` cannot infer `T` (the response type appears only in the return), so name it explicitly: `GrpcCallBuilder.Faulted\u003cMyResponse\u003e(...)`.\n\nTo fault with a pre-built `RpcException` (for example to attach trailers), use the `Faulted\u003cT\u003e(RpcException)` overload:\n\n```csharp\nvar ex = new RpcException(new Status(StatusCode.NotFound, \"no such greeting\"));\nreturn GrpcCallBuilder.Faulted\u003cMyResponse\u003e(ex);\n```\n\n### When *not* to use `ThrowsGrpcException`\n\n`ThrowsGrpcException(code)` asserts the call threw an `RpcException` carrying a given status. That is the right contract for \"the wrapper translates a failure into this status.\" It is the wrong contract for a test that asserts the wrapper rethrows the *same* `RpcException` instance it received: matching the status code is weaker than asserting reference identity, so migrating such a test to `ThrowsGrpcException(code)` would silently weaken it.\n\nIdentity-propagation tests stay on `Throws\u003cRpcException\u003e()` plus `IsSameReferenceAs`:\n\n```csharp\nvar thrown = new RpcException(new Status(StatusCode.Internal, \"boom\"));\nvar sut = new RetryingGreeterClient(new FakeGreeterClient(reply: null!, throwOnCall: thrown));\n\nvar caught = await Assert.That(() =\u003e sut.SayHelloAsync(request, ct)).Throws\u003cRpcException\u003e();\nawait Assert.That(caught!).IsSameReferenceAs(thrown);\n```\n\nThe point is that the exact instance propagated unchanged: same status, same trailers, same stack, no re-wrapping. Use `ThrowsGrpcException(code)` when you care that the *status* is correct; keep `Throws\u003cRpcException\u003e()` + `IsSameReferenceAs` when you care that the *instance* is preserved.\n\n### Await the call against a generated client\n\nThe delegate forms in this README assume `client` is a wrapper whose method returns a `Task` (or `Task\u003cT\u003e`), which the assertion awaits. A *generated* gRPC client is different: its `XAsync` method returns `AsyncUnaryCall\u003cT\u003e`, and the failure lives in the call's `ResponseAsync`, not in constructing the call. A delegate that just returns the call is not awaited, so the fault never surfaces: `ThrowsGrpcException` reports \"no exception was thrown\" and `DoesNotThrowGrpcException` passes for the wrong reason.\n\n```csharp\n// Footgun: the AsyncUnaryCall is returned but never awaited, so a faulted call looks like success.\nawait Assert.That(() =\u003e generatedClient.GetOrderAsync(request)).ThrowsGrpcException();\n\n// Correct: await the call, or assert on its ResponseAsync.\nawait Assert.That(async () =\u003e await generatedClient.GetOrderAsync(request)).ThrowsGrpcException();\nawait Assert.That(() =\u003e generatedClient.GetOrderAsync(request).ResponseAsync).ThrowsGrpcException();\n```\n\nThis package is built for testing client *wrappers* (which return `Task`), so the wrapper examples above need no change; the note matters only when you assert directly against a raw generated client.\n\n**Assert a specific failure, status and detail in one chain:**\n\n```csharp\nawait Assert.That(() =\u003e client.GetOrderAsync(request, ct))\n    .ThrowsGrpcException(StatusCode.InvalidArgument)\n    .WithDetailContaining(\"field 'id' is required\", StringComparison.Ordinal);\n```\n\n**Assert any gRPC failure without pinning the status** (when the status is environment-dependent):\n\n```csharp\nawait Assert.That(() =\u003e client.GetOrderAsync(request, ct)).ThrowsGrpcException();\n```\n\n**Assert a benign-error swallow** (the client catches a known `RpcException` and completes):\n\n```csharp\nawait Assert.That(() =\u003e client.CancelOrderAsync(request, ct)).DoesNotThrowGrpcException();\n```\n\n**Remove the five-parameter constructor from a gRPC client fake:**\n\n```csharp\n// before: new AsyncUnaryCall\u003cOrderReply\u003e(Task.FromResult(reply), Task.FromResult(new Metadata()),\n//             () =\u003e new Status(StatusCode.OK, \"\"), () =\u003e new Metadata(), () =\u003e { });\n// after:\npublic override AsyncUnaryCall\u003cOrderReply\u003e GetOrderAsync(OrderRequest request, CallOptions options)\n    =\u003e _fail ? GrpcCallBuilder.Faulted\u003cOrderReply\u003e(StatusCode.Unavailable, \"server down\")\n             : GrpcCallBuilder.Success(_reply);\n```\n\n**Discriminate a caught exception before inspecting it:**\n\n```csharp\nvar caught = await Assert.That(() =\u003e client.GetOrderAsync(request, ct)).Throws\u003cException\u003e();\nawait Assert.That(caught!).IsRpcException();\n```\n\n## Design notes\n\n- **Assertions on delegates, not on `RpcException` instances.** The primary entry point is `Assert.That(() =\u003e client.Method(...))`. The library executes the call and inspects the thrown `RpcException`, cleaner than catching the exception in the test. A non-`RpcException` throw, or nothing thrown, fails the assertion rather than the test.\n- **`Grpc.Core.Api` only.** The package depends on `Grpc.Core.Api` (the minimal API surface containing `RpcException`, `StatusCode`, `Status`, `Metadata`), not `Grpc.Net.Client` or `Google.Protobuf`, so it works with any gRPC implementation. The dependency is intrinsic: the public surface is typed against the consumer's real `RpcException`, so a home-grown enum would not compile against thrown exceptions.\n- **No Protobuf dependency.** The library asserts on gRPC transport-level outcomes (status, detail), not on Protobuf message structure. Assert on response message fields with standard TUnit assertions on the deserialized response object.\n- **Explicit `StringComparison` on detail assertions.** `WithDetailContaining` requires a `StringComparison`, matching the convention across the assertion family. `WithDetail` is exact and ordinal.\n- **`GrpcCallBuilder` is test infrastructure, not an assertion.** It builds `AsyncUnaryCall\u003cT\u003e` instances for fakes and lives in the framework-agnostic core, so a future non-TUnit adapter reuses it. Both builders guard their arguments with `ArgumentNullException.ThrowIfNull`, unlike hand-rolled fakes that dereference null later.\n- **No runtime reflection** in the assertion path; AOT-clean and trimmable.\n\n## Stability intent (pre-1.0)\n\nEvery release through 1.0 is **additive**. The public API of both assemblies is pinned by a snapshot test (`PublicApiTests`) that fails on any change to a public type, member, signature, attribute, or visibility, and `EnablePackageValidation` strict-mode ApiCompat validates each release against its previous baseline at pack time. New surface is added; existing surface is not reshaped within a 0.x line. The 1.0.0 release locks the SemVer contract.\n\n## Roadmap\n\nScoped to what real consumer suites use; later minor releases add surface as demand appears. All additive.\n\n- **0.2.0**: trailer assertions (`HasTrailer`, `DoesNotHaveTrailer`, `HasTrailerCount`), response-header metadata assertions, `WithoutDetail()`, and `IsNotStatusCode(StatusCode)`.\n- **0.3.0**: server-streaming assertions (`StreamsAtLeast`, `StreamsExactly`, `StreamContains`, `AndStreamItems\u003cT\u003e`).\n- **0.4.0**: deadline and cancellation assertions (`ThrowsDeadlineExceeded`, `ThrowsCancelled`, `CompletesWithin(TimeSpan, TimeProvider)` per the cross-family `TimeProvider` convention).\n- **1.0.0**: stable SemVer contract, full snapshot coverage, and an optional `GrpcAssertions.Analyzers` package.\n\n## Family compatibility\n\nThe nine assertion-family packages: `LogAssertions.TUnit`, `TimeAssertions.TUnit`, `SnapshotAssertions.TUnit`, `MathAssertions.TUnit`, `JsonAssertions.TUnit`, `SseAssertions.TUnit`, `GrpcAssertions.TUnit`, `TracingAssertions.TUnit`, and `MetricsAssertions.TUnit`: release independently and target the same .NET TFM at any moment (LTS-anchored, multi-target during STS support windows; see the [TFM policy in CONVENTIONS.md](CONVENTIONS.md#tfm-policy) for the rotation schedule). **Mix versions freely.** Each package ships under SemVer with `EnablePackageValidation` strict-mode ApiCompat against its previous baseline, so binary breaks within a version line are caught at pack time.\n\nFor per-package release notes:\n- [LogAssertions.TUnit CHANGELOG](https://github.com/JohnVerheij/LogAssertions.TUnit/blob/main/CHANGELOG.md)\n- [TimeAssertions.TUnit CHANGELOG](https://github.com/JohnVerheij/TimeAssertions.TUnit/blob/main/CHANGELOG.md)\n- [SnapshotAssertions.TUnit CHANGELOG](https://github.com/JohnVerheij/SnapshotAssertions.TUnit/blob/main/CHANGELOG.md)\n- [MathAssertions.TUnit CHANGELOG](https://github.com/JohnVerheij/MathAssertions.TUnit/blob/main/CHANGELOG.md)\n- [JsonAssertions.TUnit CHANGELOG](https://github.com/JohnVerheij/JsonAssertions.TUnit/blob/main/CHANGELOG.md)\n- [SseAssertions.TUnit CHANGELOG](https://github.com/JohnVerheij/SseAssertions.TUnit/blob/main/CHANGELOG.md)\n- [GrpcAssertions.TUnit CHANGELOG](https://github.com/JohnVerheij/GrpcAssertions.TUnit/blob/main/CHANGELOG.md)\n- [TracingAssertions.TUnit CHANGELOG](https://github.com/JohnVerheij/TracingAssertions.TUnit/blob/main/CHANGELOG.md)\n- [MetricsAssertions.TUnit CHANGELOG](https://github.com/JohnVerheij/MetricsAssertions.TUnit/blob/main/CHANGELOG.md)\n\n## Pair with\n\n- **[`LogAssertions.TUnit`](https://www.nuget.org/packages/LogAssertions.TUnit/)**: fluent log assertions over `Microsoft.Extensions.Logging.Testing.FakeLogCollector`.\n- **[`TimeAssertions.TUnit`](https://www.nuget.org/packages/TimeAssertions.TUnit/)**: `TimeProvider`-aware time assertions and cross-cutting `.WithinTimeBudget(...)` chain methods.\n- **[`SnapshotAssertions.TUnit`](https://www.nuget.org/packages/SnapshotAssertions.TUnit/)**: text-snapshot assertions for API-surface tests and similar deterministic-string scenarios. Coexists with Verify; covers the 80% case without coverage friction.\n- **[`MathAssertions.TUnit`](https://www.nuget.org/packages/MathAssertions.TUnit/)**: tolerance-aware fluent assertions over numeric and geometric types (vectors, quaternions, matrices, planes, complex numbers, arrays).\n- **[`JsonAssertions.TUnit`](https://www.nuget.org/packages/JsonAssertions.TUnit/)**: fluent JSON assertions over `System.Text.Json`, HTTP response bodies (including RFC 7807 ProblemDetails), and source-generated `JsonSerializerContext` registration.\n- **[`SseAssertions.TUnit`](https://www.nuget.org/packages/SseAssertions.TUnit/)**: Server-Sent Events wire-format and stream assertions over HTTP response bodies, streams, and strings.\n- **[`TracingAssertions.TUnit`](https://www.nuget.org/packages/TracingAssertions.TUnit/)**: fluent OpenTelemetry distributed-tracing (`Activity` / span) assertions: operation name, tags, status, and parent/child and same-trace relationships, captured via a raw `ActivityListener` with no OpenTelemetry SDK dependency.\n- **[`MetricsAssertions.TUnit`](https://www.nuget.org/packages/MetricsAssertions.TUnit/)**: fluent assertions over `System.Diagnostics.Metrics` instruments (counters, histograms, gauges), built on `MetricCollector`.\n\n## Contributing\n\nIssues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the build, test, and snapshot-acceptance workflow, and [CONVENTIONS.md](CONVENTIONS.md) for the family-wide structure and policy. By participating you agree to the [Code of Conduct](CODE_OF_CONDUCT.md).\n\n## License\n\n[MIT](LICENSE). Takes a single runtime dependency on `Grpc.Core.Api` (Apache-2.0).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnverheij%2Fgrpcassertions.tunit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjohnverheij%2Fgrpcassertions.tunit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnverheij%2Fgrpcassertions.tunit/lists"}