-
Notifications
You must be signed in to change notification settings - Fork 2
Unit Testing
Cookbook of test examples using InMemoryCosmos.Create() — no Dependency Injection required. For wiring details (multi-container, hierarchical partition keys, handler wrapping, custom serializers), see the Setup Guide — Direct Instantiation. For Dependency Injection-based integration tests, see the Setup Guide.
InMemoryCosmos.Create() returns an InMemoryCosmosResult with a real CosmosClient backed by in-memory storage — your code under test uses the standard SDK Container without changes.
using CosmosDB.InMemoryEmulator;
using Microsoft.Azure.Cosmos;
using var cosmos = InMemoryCosmos.Create("my-container", "/partitionKey");Both arguments after the container name are optional (partition key defaults to "/id"), but in practice you'll want to specify the partition key path to match your production container. The container is ready to use immediately.
For composite (hierarchical) partition keys:
using var cosmos = InMemoryCosmos.Create("my-container", new[] { "/tenantId", "/userId" });Or for advanced settings like unique key policies, use the configureContainer callback:
using var cosmos = InMemoryCosmos.Create("my-container", "/partitionKey",
configureContainer: c =>
{
c.UniqueKeyPolicy = new UniqueKeyPolicy
{
UniqueKeys = { new UniqueKey { Paths = { "/email" } } }
};
});[Fact]
public async Task CreateAndRead_RoundTrips()
{
using var cosmos = InMemoryCosmos.Create("orders", "/customerId");
// Create
var order = new { id = "1", customerId = "cust-1", product = "Widget", price = 9.99 };
var createResponse = await cosmos.Container.CreateItemAsync(order, new PartitionKey("cust-1"));
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
// Read
var readResponse = await cosmos.Container.ReadItemAsync<dynamic>("1", new PartitionKey("cust-1"));
Assert.Equal("Widget", (string)readResponse.Resource.product);
// Upsert
var updated = new { id = "1", customerId = "cust-1", product = "Widget", price = 12.99 };
await cosmos.Container.UpsertItemAsync(updated, new PartitionKey("cust-1"));
// Delete
await cosmos.Container.DeleteItemAsync<dynamic>("1", new PartitionKey("cust-1"));
}[Fact]
public async Task Query_FiltersCorrectly()
{
using var cosmos = InMemoryCosmos.Create("products", "/category");
await cosmos.Container.CreateItemAsync(
new { id = "1", category = "electronics", name = "Laptop", price = 999 },
new PartitionKey("electronics"));
await cosmos.Container.CreateItemAsync(
new { id = "2", category = "electronics", name = "Mouse", price = 25 },
new PartitionKey("electronics"));
await cosmos.Container.CreateItemAsync(
new { id = "3", category = "books", name = "C# in Depth", price = 40 },
new PartitionKey("books"));
var query = new QueryDefinition("SELECT * FROM c WHERE c.price > @min")
.WithParameter("@min", 30);
var iterator = cosmos.Container.GetItemQueryIterator<dynamic>(query);
var results = new List<dynamic>();
while (iterator.HasMoreResults)
{
var page = await iterator.ReadNextAsync();
results.AddRange(page);
}
Assert.Equal(2, results.Count);
}See SQL Queries for the full reference — 120+ built-in functions, JOINs, GROUP BY, aggregates, and more.
[Fact]
public async Task Linq_FiltersAndProjects()
{
using var cosmos = InMemoryCosmos.Create("orders", "/customerId");
await cosmos.Container.CreateItemAsync(
new Order { Id = "1", CustomerId = "cust-1", Product = "Widget", Price = 9.99m },
new PartitionKey("cust-1"));
await cosmos.Container.CreateItemAsync(
new Order { Id = "2", CustomerId = "cust-1", Product = "Gadget", Price = 49.99m },
new PartitionKey("cust-1"));
var expensive = cosmos.Container.GetItemLinqQueryable<Order>(true)
.Where(o => o.Price > 20)
.ToList();
Assert.Single(expensive);
Assert.Equal("Gadget", expensive[0].Product);
}Note:
InMemoryCosmosusesFakeCosmosHandlerunder the hood, so LINQ queries go through the SDK's full LINQ-to-SQL translation pipeline. See also LINQ Support and Known Limitations: LINQ Queryable Options Ignored.
The most common pattern — your repository depends on Container, and you get one from InMemoryCosmos:
// Production code (unchanged)
public class OrderRepository
{
private readonly Container _container;
public OrderRepository(Container container) => _container = container;
public async Task<Order> CreateAsync(Order order)
{
var response = await _container.CreateItemAsync(order, new PartitionKey(order.CustomerId));
return response.Resource;
}
public async Task<List<Order>> GetByCustomerAsync(string customerId)
{
var query = new QueryDefinition("SELECT * FROM c WHERE c.customerId = @id")
.WithParameter("@id", customerId);
var iterator = _container.GetItemQueryIterator<Order>(query);
var results = new List<Order>();
while (iterator.HasMoreResults)
{
var page = await iterator.ReadNextAsync();
results.AddRange(page);
}
return results;
}
}
// Test
public class OrderRepositoryTests
{
[Fact]
public async Task GetByCustomer_ReturnsOnlyMatchingOrders()
{
using var cosmos = InMemoryCosmos.Create("orders", "/customerId");
var repo = new OrderRepository(cosmos.Container);
await repo.CreateAsync(new Order { Id = "1", CustomerId = "cust-1", Product = "Widget" });
await repo.CreateAsync(new Order { Id = "2", CustomerId = "cust-2", Product = "Gadget" });
await repo.CreateAsync(new Order { Id = "3", CustomerId = "cust-1", Product = "Sprocket" });
var results = await repo.GetByCustomerAsync("cust-1");
Assert.Equal(2, results.Count);
Assert.All(results, o => Assert.Equal("cust-1", o.CustomerId));
}
}When your test needs multiple containers, use InMemoryCosmos.Builder():
[Fact]
public async Task CrossContainerTest()
{
using var cosmos = InMemoryCosmos.Builder()
.AddContainer("orders", "/customerId")
.AddContainer("customers", "/id")
.Build();
await cosmos.Containers["customers"].CreateItemAsync(
new { id = "c1", name = "Alice" }, new PartitionKey("c1"));
await cosmos.Containers["orders"].CreateItemAsync(
new { id = "1", customerId = "c1", product = "Widget" }, new PartitionKey("c1"));
var customer = await cosmos.Containers["customers"].ReadItemAsync<dynamic>("c1", new PartitionKey("c1"));
Assert.Equal("Alice", (string)customer.Resource.name);
}Patch operations let you update individual properties without replacing the entire document:
[Fact]
public async Task Patch_UpdatesSingleProperty()
{
using var cosmos = InMemoryCosmos.Create("orders", "/customerId");
await cosmos.Container.CreateItemAsync(
new { id = "1", customerId = "cust-1", status = "pending" },
new PartitionKey("cust-1"));
await cosmos.Container.PatchItemAsync<dynamic>("1", new PartitionKey("cust-1"),
new[] { PatchOperation.Set("/status", "shipped") });
var response = await cosmos.Container.ReadItemAsync<dynamic>("1", new PartitionKey("cust-1"));
Assert.Equal("shipped", (string)response.Resource.status);
}Every write returns an ETag that you can use for optimistic concurrency control:
[Fact]
public async Task Replace_WithETag_DetectsConflict()
{
using var cosmos = InMemoryCosmos.Create("orders", "/customerId");
var created = await cosmos.Container.CreateItemAsync(
new { id = "1", customerId = "cust-1", status = "pending" },
new PartitionKey("cust-1"));
var etag = created.ETag;
// First replace succeeds
await cosmos.Container.ReplaceItemAsync(
new { id = "1", customerId = "cust-1", status = "shipped" }, "1",
new PartitionKey("cust-1"),
new ItemRequestOptions { IfMatchEtag = etag });
// Second replace with the stale ETag fails
var ex = await Assert.ThrowsAsync<CosmosException>(() =>
cosmos.Container.ReplaceItemAsync(
new { id = "1", customerId = "cust-1", status = "cancelled" }, "1",
new PartitionKey("cust-1"),
new ItemRequestOptions { IfMatchEtag = etag }));
Assert.Equal(HttpStatusCode.PreconditionFailed, ex.StatusCode);
}Configure container-level settings via the configureContainer callback on Create() or AddContainer().
Set DefaultTimeToLive (in seconds) to enable automatic item expiration:
using var cosmos = InMemoryCosmos.Create("orders", "/customerId",
configureContainer: c => c.DefaultTimeToLive = 3600);Set FeedRangeCount to simulate multiple physical partitions for testing FeedRange-scoped queries and change feed iterators:
using var cosmos = InMemoryCosmos.Create("events", "/tenantId",
configureContainer: c => c.FeedRangeCount = 4);Each InMemoryCosmos.Create() or Builder().Build() call returns a fresh, independent setup, so creating one per test provides clean isolation automatically. If you share a result across tests (e.g. via a fixture), call ClearItems() to reset state:
cosmos.ClearItems(); // removes all documentsFor snapshot-based testing, you can export and import the full container state as JSON:
var snapshot = cosmos.ExportState();
// ... run operations that modify data ...
cosmos.ClearItems();
cosmos.ImportState(snapshot); // restore to the saved statepublic class OrderTests : IAsyncLifetime, IAsyncDisposable
{
private readonly InMemoryCosmosResult _cosmos =
InMemoryCosmos.Create("orders", "/customerId");
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => _cosmos.DisposeAsync().AsTask();
[Fact]
public async Task Can_create_and_read_order()
{
await _cosmos.Container.CreateItemAsync(
new Order { Id = "1", CustomerId = "c1" },
new PartitionKey("c1"));
var response = await _cosmos.Container.ReadItemAsync<Order>(
"1", new PartitionKey("c1"));
Assert.Equal("1", response.Resource.Id);
}
}If your system under test uses dependency injection, consider the Dependency Injection approach instead — it requires zero setup in individual tests and automatically replaces all Cosmos registrations. See the Setup Guide for all supported patterns.
For fault injection and query logging, access cosmos.Handler — see Choosing Your Approach for details.
If something isn't working as expected, check Troubleshooting and Known Limitations.
Getting Started
Integration & Dependency Injection
Data Management
Reference
Help