Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

History note: All version sections were reformatted on 2026-07-05 for one-time CONVENTIONS §CHANGELOG conformance: forbidden sub-headers folded into the six Keep a Changelog headers, non-user-facing content removed (internal refactors, test counts, coverage numbers, CI and build hygiene, governance churn, roadmap notes), bullets kept in past-tense active voice with code-formatted API leads. The nuget.org Release Notes tab and the GitHub Release for each shipped version are unchanged. A CI family-lint gate keeps future sections conforming; each is frozen per Rule 7 once shipped.

Unreleased

0.3.0 - 2026-06-14: server-streaming builders and assertions

Minor release. Extends the builder and the assertion surface from unary calls to server-streaming calls: GrpcCallBuilder now constructs AsyncServerStreamingCall<T> test doubles, and a Streams() chain asserts on the streamed responses. Purely additive; the 0.2.0 ApiCompat baseline is preserved.

Added

  • GrpcCallBuilder.ServerStreaming<TResponse>(IEnumerable<TResponse> responses) builds a successful server-streaming call whose response stream yields the responses in order and then ends cleanly with a terminal StatusCode.OK.
  • GrpcCallBuilder.ServerStreamingFaulted<TResponse>(StatusCode statusCode, IEnumerable<TResponse> responses, string? detail = null) builds a server-streaming call that yields the responses and then throws an RpcException on the next read, so a wrapper's mid-stream fault handling can be exercised.
  • Streams() on an AsyncServerStreamingCall<TResponse> begins a server-streaming assertion that reads the response stream once. Chain StreamsAtLeast(int) / StreamsExactly(int) to assert the response count, StreamContains(Func<TResponse, bool>) to assert at least one response matches (the predicate text is captured for the failure message), and AndStreamItems(Func<IReadOnlyList<TResponse>, Task>) to run follow-on assertions against the materialized responses without re-reading the stream. A stream that faults mid-read fails with the partial count and the rendered gRPC outcome.

0.2.0 - 2026-06-12: response and trailer metadata on the builder, trailer assertions

Minor release. Lets a faked call carry response headers and trailers, and adds trailer assertions (text and binary) to the RpcException chain. Purely additive.

Added

  • GrpcCallBuilder.Success<T>(T response, Metadata? responseHeaders, Metadata? trailers) and GrpcCallBuilder.Faulted<T>(StatusCode statusCode, string? detail, Metadata trailers) build calls that carry response metadata, so a wrapper that reads response headers or trailers can be tested against a fake. A null headers or trailers argument is treated as empty metadata.
  • WithTrailer(string key, string value) and WithTrailer(string key, ReadOnlySpan<byte> value) on the ThrowsGrpcException() chain assert the exception's Trailers contain a text or binary (-bin) entry. Keys are matched case-insensitively (gRPC lowercases metadata keys). The text overload reads Metadata.Entry.Value and the binary overload reads Metadata.Entry.ValueBytes, each guarded by Entry.IsBinary, so the value of a binary entry is never read as a string. On failure the message renders the trailers, with binary entries shown as (binary, N bytes).

Changed

  • README adds an "Await the call against a generated client" note: a generated client's XAsync returns AsyncUnaryCall<T> whose fault lives in ResponseAsync, so a delegate that returns the call without awaiting it never surfaces the fault and the assertion reports no exception. Assert via async () => await client.XAsync(...) or client.XAsync(...).ResponseAsync when testing a raw generated client; wrapper tests, which return Task, are unaffected.

Fixed

  • GrpcCallBuilder.Success<T> now returns the same trailers Metadata instance on every call to the trailers accessor, matching a real client; it previously returned a fresh empty Metadata each time, so an identity check or mutation did not behave as it would against a real call.

0.1.2 - 2026-06-05: documentation refresh

Documentation and release-tooling release. No API or behavior change.

0.1.1 - 2026-06-04: README cookbook recipes

Documentation patch. No public-surface change; the shipped assemblies are identical to v0.1.0.

Changed

  • README adds a "Replacing hand-rolled AsyncUnaryCall<T> factories" recipe as the lead Cookbook entry: a before/after that contrasts the five-parameter AsyncUnaryCall<T> constructor (response task, response-headers task, status accessor, trailers accessor, dispose callback) with GrpcCallBuilder.Success(response) and GrpcCallBuilder.Faulted<T>(...), and spells out the type-inference rule (Success<T>(T) infers T; Faulted<T>(RpcException) needs the explicit T). This is the highest-value use of the builder: deleting fake-client constructor boilerplate.
  • README adds a "When not to use ThrowsGrpcException" recipe: tests that assert the wrapper rethrows the same RpcException instance are a stronger contract than ThrowsGrpcException(code) and stay on Throws<RpcException>() plus IsSameReferenceAs, since matching only the status code would weaken an identity-propagation test.
  • The two packed package READMEs link to the new Cookbook recipes and restate the Success<T> / Faulted<T> inference rule, kept consistent with the GitHub README.

0.1.0 - 2026-06-02: gRPC outcome assertions

Feature release. Lifts the package from skeleton to functional: the gRPC outcome-assertion surface ships. ThrowsGrpcException() asserts a delegate threw an RpcException, with StatusCode shorthands and Status.Detail refinements; DoesNotThrowGrpcException() covers the benign-error swallow tests; and GrpcCallBuilder removes the five-parameter AsyncUnaryCall<T> constructor that every hand-rolled gRPC fake repeats. The v0.0.1 IsRpcException() discriminator is preserved unchanged.

Added

  • Assert.That(() => call).ThrowsGrpcException() asserts the delegate throws a gRPC RpcException of any status; ThrowsGrpcException(StatusCode expected) asserts the status. Both return a GrpcExceptionAssertion chain, and failure messages render the actual StatusCode and Status.Detail. The entry points extend the delegate assertion source produced by Assert.That(() => client.Method(...)).
  • 14 StatusCode shorthands chaining off ThrowsGrpcException(): IsOk, IsCancelled, IsInvalidArgument, IsDeadlineExceeded, IsNotFound, IsAlreadyExists, IsPermissionDenied, IsResourceExhausted, IsFailedPrecondition, IsAborted, IsUnimplemented, IsInternal, IsUnavailable, IsUnauthenticated.
  • WithDetail(string) (exact, ordinal) and WithDetailContaining(string, StringComparison) (substring, explicit comparison per the family convention) refine the assertion on Status.Detail.
  • Assert.That(() => call).DoesNotThrowGrpcException() asserts the delegate completes without throwing an RpcException. A thrown RpcException fails with the gRPC outcome rendered; any other thrown exception fails naming its type and message.
  • GrpcCallBuilder (core GrpcAssertions package): Success<T>(T), Faulted<T>(RpcException), and Faulted<T>(StatusCode, string?) build AsyncUnaryCall<T> instances for gRPC client test doubles, replacing the repeated five-parameter constructor. Both builders guard their arguments with ArgumentNullException.ThrowIfNull, a strict improvement over the hand-rolled fakes that would dereference-null later.
  • GrpcOutcomeRendering (core): Describe(RpcException) and DescribeStatus(StatusCode, string?) render the status and detail (truncated at MaxDetailLength, 200 characters, with a horizontal-ellipsis suffix) for failure messages, shared so consumer-authored gRPC assertions produce identical diagnostics.

0.0.1 - 2026-06-01: skeleton release

Skeleton release. Establishes the repository, the GrpcAssertions (core) and GrpcAssertions.TUnit (adapter) package identifiers on nuget.org, and the full family quality bar (AOT-clean with no runtime reflection in the assertion path, SBOM plus SLSA build provenance, public-API snapshot pinning). Ships a single discriminator, IsRpcException(), so the surface is real and exercised end-to-end rather than empty. The gRPC outcome-assertion surface (the GrpcCallBuilder test infrastructure plus the ThrowsGrpcException verbs and StatusCode shorthands) lands in v0.1.0.

Added

  • Assert.That(exception).IsRpcException() asserts that an Exception is a gRPC RpcException, the exception type a failed gRPC call surfaces. The failure message names the actual exception type. Source-generated via [GenerateAssertion]; the generated chain is AOT-clean.
  • GrpcExceptions.IsRpcException(Exception?) framework-agnostic predicate in the GrpcAssertions core package. Returns false for null and for any non-RpcException type. The adapter assertion is a thin wrapper over this helper, so a future xUnit, NUnit, or MSTest adapter reuses the same logic.
  • Single disclosed runtime dependency: Grpc.Core.Api (Apache-2.0), the package that defines the RpcException / StatusCode / Status types the assertions are about. It flows transitively to consumers through the core package.