> ## Documentation Index
> Fetch the complete documentation index at: https://repost.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Test C# Sends Without the Network

> The Repost.Client.Testing package: the scripted StubTransport, recorded-request assertions, and deterministic clocks and generators.

Everything a send does before the network — validation, defaults, serialization, the typed method tree — runs for real under the stub transport, so tests exercise your schema without any HTTP. The compiler is part of the test surface too: wrong payloads, unknown events, and missing fields fail at build time.

The `Repost.Client.Testing` package holds the test doubles. Add it in your test project:

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
dotnet add package Repost.Client.Testing
```

## The stub transport

`StubTransport` is a scripted, no-network `ITransport`: enqueue the responses each attempt should return, plug it into the `Transport` option, send, and assert on what it recorded. The `Authorization` header is stripped from every recording, and the body bytes are copied verbatim:

```csharp theme={"languages":{"custom":["/languages/repost.json"]}}
using Repost.Client;
using Repost.Client.Testing;
using Contoso.Events;

var transport = new StubTransport().EnqueueResponse(
    202,
    "{\"id\":\"msg_1\",\"type\":\"book.created\",\"customerId\":\"cus_123\","
        + "\"timestamp\":\"1970-01-01T00:00:00.000Z\"}");

await using var repost = new RepostClient(new RepostClientOptions
{
    ApiKey = "test-key",
    Transport = transport,
    MaxAttempts = 1,
});

SendResult result = await repost.Webhooks.Book.CreatedAsync(new BookCreatedInput
{
    CustomerId = "cus_123",
    Data = new Book
    {
        Title = "Dune",
        Authors = new[] { new Author { Name = "Frank Herbert" } },
        Price = 9.99,
        Currency = Currency.Usd,
    },
});

Assert.Equal("msg_1", result.Id);

RecordedRequest sent = transport.Requests[0];
string body = Encoding.UTF8.GetString(sent.Body);
Assert.Contains("\"title\":\"Dune\"", body);
```

Credentials still resolve before the stub runs, so tests exercise the production configuration path; any grammar-valid key satisfies it. The returned `SendResult` echoes the id, type, customer, and timestamp from the scripted response body.

<Note>
  `StubTransport` is scripted, not zero-config: an attempt with an empty script faults with `InvalidOperationException`, a raw transport defect the runtime never retries. Enqueue one response per attempt you expect.
</Note>

## Scripting failures and retries

Beyond `EnqueueResponse`, the transport scripts the other attempt shapes: `EnqueueFailure(TransportFailureException)` for a classified, retry-eligible network failure, `EnqueueException(Exception)` for a raw defect, and `EnqueuePending()` for a response your test completes later through the returned `ControlledResponse`. To drive a retry test, enqueue two `500`s and a `202`, raise `MaxAttempts`, and assert `transport.Requests.Count == 3`. `AwaitRequestCountAsync(count, timeout)` waits for in-flight sends to reach a request count.

## Deterministic values

Pin the injectable seams to freeze timestamps and generated ids. `TestDefaultValueGenerators` and `TestIdempotencyKeyGenerator` provide `Fixed`, `Sequence`, and `Failing` factories:

```csharp theme={"languages":{"custom":["/languages/repost.json"]}}
var options = new RepostClientOptions
{
    ApiKey = "test-key",
    Transport = transport,
    DefaultValueGenerators = TestDefaultValueGenerators.Fixed(
        DateTimeOffset.UnixEpoch,
        "00000000-0000-4000-8000-000000000000",
        "ck0nf0rm4nc3f1x3dcu1d000"),
    IdempotencyKeyGenerator = TestIdempotencyKeyGenerator.Fixed("book_dune:created"),
};
```

With `Now()` pinned, the envelope timestamp and every `@default(now())` field are fixed — so you can script an exact accepted-response body. The package also ships `ManualTimeProvider` (a fully deterministic clock you `Advance` to fire retry timers), `DeterministicRetryEntropy` (`Zero` or a scripted jitter sequence), and `RecordingObserver` (asserts on the lifecycle events the runtime emits).

## Continue

<Columns cols={3} className="gap-y-4">
  <Card title="Quickstart" icon="rocket" href="/docs/send/csharp/quickstart" cta="Start" arrow="true">
    The full setup, from install to first send.
  </Card>

  <Card title="Reliability" icon="shield-check" href="/docs/send/csharp/reliability" cta="Outcomes" arrow="true">
    The delivery states and error codes to exercise in your tests.
  </Card>

  <Card title="Configuration" icon="sliders-horizontal" href="/docs/send/csharp/configuration" cta="Configure" arrow="true">
    The options and transport seams in full.
  </Card>
</Columns>
