Skip to content

Testing

Aghogho Bernard edited this page May 6, 2026 · 1 revision

Testing

Unit Testing with a Fake Provider

The simplest approach is to register CacheWeave.InMemory in your test project — it uses IMemoryCache and requires no external dependencies.

using CacheWeave.Core.Extensions;
using CacheWeave.InMemory.Extensions;
using Microsoft.Extensions.DependencyInjection;

public static class TestServiceCollectionExtensions
{
    public static IServiceCollection AddCacheWeaveForTesting(
        this IServiceCollection services)
    {
        services.AddCacheWeave(options =>
        {
            options.KeyVersion = "test";
            options.DefaultExpiry = TimeSpan.FromMinutes(5);
            options.EnableMetrics = false;
        });
        services.AddCacheWeaveInMemory();
        return services;
    }
}

Use it in xUnit:

public class ProductServiceTests
{
    private readonly IServiceProvider _sp;

    public ProductServiceTests()
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddCacheWeaveForTesting();
        services.AddScoped<ProductService>();
        // ... register other dependencies
        _sp = services.BuildServiceProvider();
    }

    [Fact]
    public async Task GetAllAsync_SecondCall_ReturnsCachedResult()
    {
        var sut = _sp.GetRequiredService<ProductService>();

        var first = await sut.GetAllAsync();
        var second = await sut.GetAllAsync();

        // Both calls return the same object reference (from cache)
        Assert.Same(first, second);
    }
}

Integration Testing with WebApplicationFactory

Use WebApplicationFactory<TProgram> to spin up the full ASP.NET Core pipeline with an in-memory cache:

public class ProductsControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public ProductsControllerTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Replace the Redis provider with InMemory for tests
                services.AddCacheWeaveInMemory();
            });
        }).CreateClient();
    }

    [Fact]
    public async Task GetAll_SecondRequest_ReturnsCachedResponse()
    {
        var first = await _client.GetAsync("/api/products?page=1");
        var second = await _client.GetAsync("/api/products?page=1");

        first.EnsureSuccessStatusCode();
        second.EnsureSuccessStatusCode();

        // Both responses should have identical bodies
        var body1 = await first.Content.ReadAsStringAsync();
        var body2 = await second.Content.ReadAsStringAsync();
        Assert.Equal(body1, body2);
    }

    [Fact]
    public async Task Create_InvalidatesListCache()
    {
        // Populate the cache
        await _client.GetAsync("/api/products?page=1");

        // Mutate
        var cmd = new { name = "New Product", categoryId = Guid.NewGuid() };
        var post = await _client.PostAsJsonAsync("/api/products", cmd);
        post.EnsureSuccessStatusCode();

        // The next GET should re-fetch (cache was evicted)
        var afterCreate = await _client.GetAsync("/api/products?page=1");
        afterCreate.EnsureSuccessStatusCode();
    }
}

Mocking ICacheWeaveService

Use Moq or NSubstitute to mock ICacheWeaveService in unit tests where you want to assert cache interactions without a real provider:

using NSubstitute;

public class ProductControllerTests
{
    [Fact]
    public async Task GetAll_CacheHit_DoesNotCallDatabase()
    {
        var cache = Substitute.For<ICacheWeaveService>();
        var db = Substitute.For<IProductRepository>();

        cache.GetOrSetAsync<List<ProductDto>>(
                Arg.Any<string>(),
                Arg.Any<Func<Task<List<ProductDto>?>>>(),
                Arg.Any<TimeSpan?>(),
                Arg.Any<CancellationToken>())
            .Returns(new List<ProductDto> { new() { Id = Guid.NewGuid(), Name = "Steel Beam" } });

        var controller = new ProductsController(cache, db);
        var result = await controller.GetAll();

        Assert.IsType<OkObjectResult>(result);
        await db.DidNotReceive().GetAllAsync(Arg.Any<CancellationToken>());
    }
}

Asserting Cache Hits and Misses

Inject ICacheProvider directly in integration tests to inspect cache state:

public class CacheIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public CacheIntegrationTests(WebApplicationFactory<Program> factory)
        => _factory = factory;

    [Fact]
    public async Task GetAll_PopulatesCache()
    {
        var app = _factory.WithWebHostBuilder(b =>
            b.ConfigureServices(s => s.AddCacheWeaveInMemory()));

        var client = app.CreateClient();
        var cache = app.Services.GetRequiredService<ICacheProvider>();

        await client.GetAsync("/api/products?page=1");

        // Key format: baseKey:version:queryParams
        var cached = await cache.GetAsync<object>("products:list:test:page=1");
        Assert.NotNull(cached);
    }
}

Testing Eviction

[Fact]
public async Task Delete_EvictsCache()
{
    var app = _factory.WithWebHostBuilder(b =>
        b.ConfigureServices(s => s.AddCacheWeaveInMemory()));

    var client = app.CreateClient();
    var cache = app.Services.GetRequiredService<ICacheProvider>();

    // Populate cache
    await client.GetAsync("/api/products?page=1");

    // Delete a product
    var id = Guid.NewGuid();
    await client.DeleteAsync($"/api/products/{id}");

    // Cache should be cleared
    var cached = await cache.GetAsync<object>("products:list:test:page=1");
    Assert.Null(cached);
}

Disabling the Cache in Tests

To test the underlying logic without caching interference, register a no-op provider:

public class NoOpCacheProvider : ICacheProviderInner
{
    public Task<string?> GetAsync(string key, CancellationToken ct = default)
        => Task.FromResult<string?>(null);

    public Task SetAsync(string key, string value, TimeSpan? expiry = null, CancellationToken ct = default)
        => Task.CompletedTask;

    public Task RemoveAsync(string key, CancellationToken ct = default)
        => Task.CompletedTask;

    public Task RemoveByPrefixAsync(string prefix, CancellationToken ct = default)
        => Task.CompletedTask;
}

// In test setup:
services.AddCacheWeave();
services.AddSingleton<ICacheProviderInner, NoOpCacheProvider>();

Every request will be a cache miss and every write will be discarded — the action logic runs on every call.

Clone this wiki locally