Skip to content

Testing Cache

Deepstaging generates per-entity typed test doubles for [CacheStore] entities. The runtime also provides a generic TestCacheStore for testing custom ICacheStore interactions. No mocking libraries needed.


Generated Test Doubles (CacheStore)

Every [Cached] entity gets a Test{Entity}Cache — a typed test double with call recording, seedable state, and Reset().

For this setup:

[TypedId]
public readonly partial struct ProductId;

[Cached]
public partial record Product(ProductId Id, string Name, decimal Price);

[CacheStore]
public static partial class AppCache;

The generator produces TestProductCache : IProductCache alongside the ProductCache implementation.

Call Recording

var cache = new TestProductCache();

var id = ProductId.New();
await cache.SetAsync(id, new Product(id, "Widget", 9.99m), TimeSpan.FromMinutes(5));
await cache.GetAsync(id);
await cache.RemoveAsync(id);

await Assert.That(cache.SetCalls).Count().IsEqualTo(1);
await Assert.That(cache.SetCalls[0].Expiration).IsEqualTo(TimeSpan.FromMinutes(5));
await Assert.That(cache.GetCalls).Contains(id);
await Assert.That(cache.RemoveCalls).Contains(id);
Property Type Description
GetCalls IReadOnlyList<ProductId> Keys passed to GetAsync
SetCalls IReadOnlyList<(ProductId, Product, TimeSpan?)> Calls to SetAsync with key, value, expiration
RemoveCalls IReadOnlyList<ProductId> Keys passed to RemoveAsync
InvalidateAllCallCount int Number of InvalidateAllAsync calls (test-only convenience method)
Entries IReadOnlyDictionary<ProductId, Product> Current cache contents

InvalidateAllAsync is test-only

InvalidateAllAsync is available on TestProductCache as a convenience for clearing test state, but it is not part of the IProductCache interface. Bulk invalidation is a provider-specific operation — see Effects Composition for details.

Seeding

Pre-populate cache state without recording calls:

var cache = new TestProductCache();
var id = ProductId.New();
cache.Seed(id, new Product(id, "Widget", 9.99m));

// Seed multiple
cache.Seed(
    (id1, product1),
    (id2, product2)
);

With TestRuntime

var cache = new TestProductCache();
cache.Seed(productId, cachedProduct);

var runtime = TestAppRuntime.Create()
    .WithProductCache(cache);

var program =
    from product in AppCache.Products.Get<TestAppRuntime>(productId)
    select product;

var result = await program.RunAsync(runtime);
await Assert.That(result).IsSuccMatching(p => p.IsSome);

Reset

cache.Reset(); // Clears entries, calls, and counters

TestCacheStore (ICacheStore Infrastructure)

For testing code that interacts directly with ICacheStore (the infrastructure layer), TestCacheStore records every call and tracks cache hits and misses.

var cache = new TestCacheStore();

// Seed a cache value
cache.Seed("user:42", new UserProfile("Alice", "alice@example.com"));

// ... run your handler that reads from cache ...

// Assert cache was consulted
await Assert.That(cache.GetCalls).Contains("user:42");
await Assert.That(cache.Hits).IsEqualTo(1);
await Assert.That(cache.Misses).IsEqualTo(0);

Call Recording

Property Type Description
GetCalls IReadOnlyList<string> All keys passed to GetAsync<T>
SetCalls IReadOnlyList<CacheSetCall> All SetAsync<T> calls with key, type, value, and expiration
RemoveCalls IReadOnlyList<string> All keys passed to RemoveAsync
ExistsCalls IReadOnlyList<string> All keys passed to ExistsAsync
Hits int Number of GetAsync<T> calls that returned a value
Misses int Number of GetAsync<T> calls that returned null

CacheSetCall Record

public record CacheSetCall(string Key, Type ValueType, object Value, TimeSpan? Expiration);

Seeding

cache.Seed("user:42", new UserProfile("Alice", "alice@example.com"));
cache.Seed("settings", new AppSettings { Theme = "dark" });

Seeded values are stored using JSON serialization, matching real cache behavior.

Asserting Cache Writes

var cache = new TestCacheStore();

// ... run handler that caches a result ...

await Assert.That(cache.SetCalls).Count().IsEqualTo(1);
await Assert.That(cache.SetCalls[0].Key).IsEqualTo("user:42");
await Assert.That(cache.SetCalls[0].Expiration).IsEqualTo(TimeSpan.FromMinutes(5));

Reset

cache.Reset(); // Clears calls, counters, and seeded values