-
Notifications
You must be signed in to change notification settings - Fork 2
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.
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.
public Func<HttpRequestMessage, HttpResponseMessage?>? FaultInjector { get; set; }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
};| 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; |
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.
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;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);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);Every HTTP request processed by FakeCosmosHandler is recorded in RequestLog, letting you verify exactly which operations your code performed.
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
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 inRequestLog, so you can verify that requests were attempted even when faults are active.
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.
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"
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 inRequestLog. If a fault is injected before the query is parsed, the query will not appear inQueryLogeither.
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);
}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.
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();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;
};
});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();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"));
}Getting Started
Integration & Dependency Injection
Data Management
Reference
Help