Skip to content

Testing caches

Compose a real service collection with AddMemoryCache, the generated caches and the generated handlers, then resolve and exercise the cache class.

2 min read

Caches are not worth mocking — the interesting behavior (coalescing, invalidation, expiry) lives in the base class. Build a real ServiceCollection, which is exactly what the package’s own functional tests do.

The composition recipe

GetValueCacheTests.cs
public sealed class GetValueCacheTests
{
	private readonly ServiceProvider _serviceProvider;

	public GetValueCacheTests()
	{
		var services = new ServiceCollection();
		_ = services.AddMemoryCache();
		_ = services.AddApplicationCaches();
		_ = services.AddApplicationHandlers();

		_serviceProvider = services.BuildServiceProvider();
	}

	[Fact]
	public async Task GetValueCachesValue()
	{
		var cache = _serviceProvider.GetRequiredService<GetValueCache>();

		var request = new GetValue.Query(Value: 1);
		var response = await cache.GetValue(request, TestContext.Current.CancellationToken);

		Assert.Equal(1, response.Value);

		var memoryCache = _serviceProvider.GetRequiredService<IMemoryCache>();
		Assert.True(memoryCache.TryGetValue($"GetValue(query: {request.Value})", out _));
	}
}

Three registrations are required and their order does not matter:

CallWhy
AddMemoryCache()Supplies the IMemoryCache every cache class takes in its constructor
AddXxxCaches()Registers the cache classes and the open generic Owned<>
AddXxxHandlers()Registers IHandler<TRequest, TResponse>, which the cache resolves per execution

Asserting on the memory cache

IMemoryCache.TryGetValue tells you whether an entry exists for a key, which is enough to assert that TransformKey produced what you expected. Do not assert on the entry’s value: it is an internal wrapper type, not your TResponse. Assert on what GetValue returns instead.

Proving that the handler ran (or did not)

Give the response a flag, or count executions in a test double, and use SetValue to prime the cache:

[Fact]
public async Task SetValueBypassesHandler()
{
	var cache = _serviceProvider.GetRequiredService<GetValueCache>();

	var request = new GetValue.Query(Value: 2);
	cache.SetValue(request, new GetValue.Response(4, ExecutedHandler: false));

	var response = await cache.GetValue(request, TestContext.Current.CancellationToken);

	Assert.Equal(4, response.Value);
	Assert.False(response.ExecutedHandler);
}

Substituting the handler

The cache resolves IHandler<TRequest, TResponse> from a fresh scope on every execution, so you can skip AddXxxHandlers() and register a stub against that interface:

services.AddScoped<IHandler<GetValue.Query, GetValue.Response>, FakeGetValueHandler>();

This gives you a handler you fully control while keeping the real caching behavior under test.

Testing concurrency

To exercise coalescing, cancellation and TransformValue retries, make the handler block on a TaskCompletionSource that the test releases. Start two GetValue calls, assert neither task has completed, release the handler, and assert both callers received the same response instance — that is how the package verifies that one execution serves both callers. The same technique proves the TransformValue retry: hold the transformer open, call SetValue underneath it, release it, and observe the transformer run a second time.

Next: the API reference.