Skip to content

Unit Testing

Aryeh Citron edited this page Apr 20, 2026 · 8 revisions

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.

Basic Setup

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" } } }
        };
    });

CRUD Operations

[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"));
}

SQL Queries

[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.

LINQ Queries

[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: InMemoryCosmos uses FakeCosmosHandler under 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.

Testing a Repository

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));
    }
}

Multi-Container Setup

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

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);
}

ETag & Optimistic Concurrency

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);
}

Container Configuration

Configure container-level settings via the configureContainer callback on Create() or AddContainer().

TTL

Set DefaultTimeToLive (in seconds) to enable automatic item expiration:

using var cosmos = InMemoryCosmos.Create("orders", "/customerId",
    configureContainer: c => c.DefaultTimeToLive = 3600);

FeedRange Count

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);

Test Isolation

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 documents

For 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 state

Test Fixture Pattern

public 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);
    }
}

When to Use Dependency Injection Instead

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.

Clone this wiki locally