Skip to content

Testing Utilities

Aryeh Citron edited this page Apr 30, 2026 · 1 revision

Testing Utilities

The emulator provides built-in testing utilities through FakeCosmosHandler for fault injection, request/query logging, SDK compatibility checks, and direct backing-container access. These features let you simulate real-world failure conditions and inspect exactly what your code sends to Cosmos DB — without changing any production code.


Fault Injection

Simulate transient failures, throttling, and timeouts by setting the FaultInjector delegate on a FakeCosmosHandler. When the delegate returns a non-null HttpResponseMessage, that response is returned immediately to the SDK — bypassing normal processing. Return null to let the request proceed normally.

Delegate Signature

public Func<HttpRequestMessage, HttpResponseMessage?>? FaultInjector { get; set; }

Basic Usage

using var cosmos = InMemoryCosmos.Create("orders", "/customerId");

cosmos.Handler.FaultInjector = request =>
{
    if (request.Method == HttpMethod.Post)
    {
        return new HttpResponseMessage((HttpStatusCode)429)
        {
            Content = new StringContent("{}"),
            Headers = { RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromMilliseconds(1)) }
        };
    }
    return null; // No fault — proceed normally
};

Common Fault Patterns

Pattern Example
429 Throttle new HttpResponseMessage((HttpStatusCode)429) { Content = new StringContent("{}"), Headers = { RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromMilliseconds(100)) } }
503 Service Unavailable new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)
500 Internal Server Error new HttpResponseMessage(HttpStatusCode.InternalServerError)
Selective by HTTP method req.Method == HttpMethod.Get ? null : new HttpResponseMessage(...)
Selective by URI / document req.RequestUri?.AbsolutePath.Contains("target-doc") == true ? fault : null
Count-based (first N fail) Interlocked.Increment(ref callCount) <= 3 ? fault : null
Clear fault handler.FaultInjector = null;

Metadata Requests

By default, metadata requests (account info, collection metadata, partition key ranges) bypass the FaultInjector so SDK initialisation is not disrupted. To also fault metadata routes, set:

cosmos.Handler.FaultInjectorIncludesMetadata = true;

Tip: Leave this false (the default) unless you specifically need to test what happens when the SDK can't initialise at all.

Dynamic Toggling

You can enable and disable fault injection at any point during a test:

// Start with no fault — operations succeed
var item = await container.ReadItemAsync<Order>("1", new PartitionKey("cust-1"));

// Enable fault — operations fail
cosmos.Handler.FaultInjector = _ => new HttpResponseMessage((HttpStatusCode)429)
{
    Content = new StringContent("{}"),
    Headers = { RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromMilliseconds(1)) }
};

var act = () => container.UpsertItemAsync(order, new PartitionKey("cust-1"));
await act.Should().ThrowAsync<CosmosException>()
    .Where(ex => ex.StatusCode == (HttpStatusCode)429);

// Disable fault — operations succeed again
cosmos.Handler.FaultInjector = null;

Multi-Container Fault Injection

For multi-container setups, inject faults per container or globally across all containers:

using var cosmos = InMemoryCosmos.Builder()
    .AddContainer("orders", "/customerId")
    .AddContainer("products", "/categoryId")
    .Build();

// Per-container fault injection
cosmos.GetHandler("orders").FaultInjector = req =>
    new HttpResponseMessage(HttpStatusCode.ServiceUnavailable);

// All containers at once
cosmos.SetFaultInjector(req =>
    new HttpResponseMessage((HttpStatusCode)429)
    {
        Content = new StringContent("{}"),
        Headers = { RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromMilliseconds(1)) }
    });

// Clear all
cosmos.SetFaultInjector(null);

Fault Injection via Dependency Injection

When using UseInMemoryCosmosDB(), use the OnHandlerCreated callback to configure fault injection:

FakeCosmosHandler? capturedHandler = null;

services.UseInMemoryCosmosDB(o =>
{
    o.AddContainer("orders", "/customerId");
    o.OnHandlerCreated = (containerName, handler) =>
    {
        capturedHandler = handler;
    };
});

// Later in your test...
capturedHandler!.FaultInjector = req =>
    new HttpResponseMessage(HttpStatusCode.ServiceUnavailable);

Request Logging

Every HTTP request processed by FakeCosmosHandler is recorded in RequestLog, letting you verify exactly which operations your code performed.

Property

public IReadOnlyCollection<string> RequestLog { get; }

Each entry is a string in the format "METHOD /path", for example:

  • "POST /dbs/db/colls/orders/docs" — Create
  • "GET /dbs/db/colls/orders/docs/item-1" — Point read
  • "PUT /dbs/db/colls/orders/docs/item-1" — Replace
  • "PATCH /dbs/db/colls/orders/docs/item-1" — Patch
  • "DELETE /dbs/db/colls/orders/docs/item-1" — Delete

Usage

using var cosmos = InMemoryCosmos.Create("orders", "/customerId");
var container = cosmos.Container;

await container.CreateItemAsync(order, new PartitionKey("cust-1"));
await container.ReadItemAsync<Order>("1", new PartitionKey("cust-1"));
await container.ReplaceItemAsync(order, "1", new PartitionKey("cust-1"));
await container.DeleteItemAsync<Order>("1", new PartitionKey("cust-1"));

// Verify the operations
cosmos.Handler.RequestLog.Should().Contain(e => e.StartsWith("POST"));
cosmos.Handler.RequestLog.Should().Contain(e => e.StartsWith("GET"));
cosmos.Handler.RequestLog.Should().Contain(e => e.StartsWith("PUT"));
cosmos.Handler.RequestLog.Should().Contain(e => e.StartsWith("DELETE"));

Note: Faulted requests (via FaultInjector) are still recorded in RequestLog, so you can verify that requests were attempted even when faults are active.


Query Logging

Every SQL query executed through FakeCosmosHandler is recorded in QueryLog, capturing the raw SQL text sent by the SDK. This includes both explicit SQL queries and LINQ expressions that the SDK translates to SQL.

Property

public IReadOnlyCollection<string> QueryLog { get; }

Each entry is the raw SQL string, for example:

  • "SELECT * FROM c"
  • "SELECT * FROM c WHERE c.status = 'active'"
  • "SELECT * FROM c ORDER BY c.name"

Usage

using var cosmos = InMemoryCosmos.Create("orders", "/customerId");
var container = cosmos.Container;

// LINQ query — SDK translates to SQL, which gets logged
var queryable = container.GetItemLinqQueryable<Order>()
    .Where(o => o.Status == "active");
var iterator = queryable.ToFeedIterator();
while (iterator.HasMoreResults)
    await iterator.ReadNextAsync();

cosmos.Handler.QueryLog.Should().NotBeEmpty();
cosmos.Handler.QueryLog.Should().Contain(q => q.Contains("active"));
// Explicit SQL query
var feedIterator = container.GetItemQueryIterator<Order>(
    "SELECT * FROM c WHERE c.total > 100");
while (feedIterator.HasMoreResults)
    await feedIterator.ReadNextAsync();

cosmos.Handler.QueryLog.Should().Contain(q => q.Contains("c.total > 100"));

Note: Pure CRUD operations (Create, Read, Replace, Delete, Patch) do not appear in QueryLog — only in RequestLog. If a fault is injected before the query is parsed, the query will not appear in QueryLog either.


SDK Compatibility

Version Warnings

FakeCosmosHandler checks the installed Cosmos SDK version against its tested range and records any warnings:

public IReadOnlyList<string> SdkVersionWarnings { get; }
if (cosmos.Handler.SdkVersionWarnings.Count > 0)
{
    foreach (var warning in cosmos.Handler.SdkVersionWarnings)
        Console.WriteLine(warning);
}

Compatibility Verification

Run a self-test against the installed SDK to detect breaking changes before they cause confusing test failures:

await FakeCosmosHandler.VerifySdkCompatibilityAsync();

This method exercises the SDK pipeline end-to-end and throws if any fundamental incompatibility is detected.

Unrecognised Headers

New SDK versions may introduce new x-ms-* headers. The handler tracks any it doesn't recognise:

public IReadOnlyCollection<string> UnrecognisedHeaders { get; }
// After running your tests, check for unknown headers
cosmos.Handler.UnrecognisedHeaders.Should().BeEmpty();

Backing Container Access

For test setup and verification, you can access the underlying InMemoryContainer directly through the handler:

// Via the handler
Container backing = cosmos.Handler.BackingContainer;

// Or via SetupContainer (preferred) for registering UDFs, triggers, etc.
IContainerTestSetup setup = cosmos.SetupContainer();
setup.RegisterUdf("myUdf", args => /* ... */);
setup.RegisterStoredProcedure("mySproc", (args, context) => /* ... */);

Via DI callbacks:

services.UseInMemoryCosmosDB(o =>
{
    o.AddContainer("orders", "/customerId");
    o.OnHandlerCreated = (containerName, handler) =>
    {
        // Seed data through the backing container
        var backing = handler.BackingContainer;
    };
});

Handler Wrapping

You can wrap the FakeCosmosHandler with a custom HttpMessageHandler (e.g., for logging, metrics, or custom middleware):

using var cosmos = InMemoryCosmos.Create("orders", "/customerId",
    wrapHandler: inner => new MyLoggingHandler(inner));

Or with the builder:

using var cosmos = InMemoryCosmos.Builder()
    .AddContainer("orders", "/customerId")
    .WrapHandler(inner => new MyLoggingHandler(inner))
    .Build();

Complete Example

Here's a full test demonstrating fault injection, request logging, and query logging together:

[Fact]
public async Task Should_retry_on_throttle_and_succeed()
{
    using var cosmos = InMemoryCosmos.Create("orders", "/customerId");
    var container = cosmos.Container;

    // Seed data
    var order = new Order { Id = "1", CustomerId = "cust-1", Status = "active" };
    await container.CreateItemAsync(order, new PartitionKey("cust-1"));

    // Inject a 429 that clears after one attempt
    var callCount = 0;
    cosmos.Handler.FaultInjector = _ =>
    {
        if (Interlocked.Increment(ref callCount) <= 1)
        {
            return new HttpResponseMessage((HttpStatusCode)429)
            {
                Content = new StringContent("{}"),
                Headers = { RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromMilliseconds(1)) }
            };
        }
        return null;
    };

    // The SDK retries automatically — this should succeed
    var response = await container.ReadItemAsync<Order>("1", new PartitionKey("cust-1"));
    response.Resource.Status.Should().Be("active");

    // Verify the retry happened
    cosmos.Handler.RequestLog.Where(r => r.StartsWith("GET")).Should().HaveCountGreaterThan(1);

    // Run a query and verify it was logged
    cosmos.Handler.FaultInjector = null;
    var iterator = container.GetItemQueryIterator<Order>("SELECT * FROM c WHERE c.status = 'active'");
    while (iterator.HasMoreResults)
        await iterator.ReadNextAsync();

    cosmos.Handler.QueryLog.Should().Contain(q => q.Contains("active"));
}

Clone this wiki locally