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