RRedis Handbook

İLERİ

Testing & Benchmarking

Gerçek API endpoint'ini Redis ile birlikte test et — en güvenilir integration test pattern'ı.

Kod örneği görünümü Bu sayfadaki eşleşen örnekleri seçilen istemciye göre gösterir.

Integration Test (Testcontainers)

dotnet add package Testcontainers.Redis
dotnet add package xunit
# redis-benchmark — yerleşik performans aracı
redis-benchmark -h localhost -p 6379 -c 50 -n 100000 -q
# SET: 125000.00 requests per second
# GET: 142857.14 requests per second

# Belirli komut benchmark
redis-benchmark -t set,get -n 100000 -d 256 -q
# -d 256: 256 byte value

# Pipeline ile (daha gerçekçi)
redis-benchmark -t set,get -n 100000 -P 16 -q
# SET: 680000+ requests per second (pipeline=16)

# Lua script benchmark
redis-benchmark -n 100000 EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1 bench:key bench:val

# Latency baseline
redis-cli --latency -i 1    # her 1 saniye örnek
redis-cli --intrinsic-latency 5   # 5sn kernel latency ölç
// Testcontainers ile integration test
using Testcontainers.Redis;
using StackExchange.Redis;
using Xunit;

public class RedisIntegrationTests : IAsyncLifetime
{
    private readonly RedisContainer _redis = new RedisBuilder()
        .WithImage("redis:8-alpine")
        .Build();

    private IConnectionMultiplexer _mux = null!;
    private IDatabase _db = null!;

    public async Task InitializeAsync()
    {
        await _redis.StartAsync();
        _mux = await ConnectionMultiplexer.ConnectAsync(
            _redis.GetConnectionString());
        _db = _mux.GetDatabase();
    }

    public async Task DisposeAsync()
    {
        _mux.Dispose();
        await _redis.DisposeAsync();
    }

    [Fact]
    public async Task CacheService_SetAndGet_ReturnsValue()
    {
        // Arrange
        var service = new CachedProductRepository(_mux);

        // Act
        await _db.StringSetAsync("product:1",
            JsonSerializer.Serialize(new { Id = 1, Name = "Test" }),
            TimeSpan.FromMinutes(1));

        var result = await _db.StringGetAsync("product:1");

        // Assert
        Assert.True(result.HasValue);
        Assert.Contains("Test", result.ToString());
    }

    [Fact]
    public async Task DistributedLock_AcquireAndRelease()
    {
        // Arrange
        var lockService = new DistributedLockService(_mux);

        // Act
        await using var handle = await lockService.AcquireAsync(
            "test-resource", TimeSpan.FromSeconds(5));

        // Assert
        Assert.NotNull(handle);

        // Lock aktifken ikinci acquire başarısız olmalı
        var second = await lockService.AcquireAsync(
            "test-resource", TimeSpan.FromSeconds(5),
            waitTimeout: TimeSpan.FromMilliseconds(100));
        Assert.Null(second);
    }

    [Fact]
    public async Task RateLimiter_ExceedsLimit_Returns429Equivalent()
    {
        var limiter = new RateLimiterService(_mux);

        // 5 request, limit=3
        for (int i = 0; i < 3; i++)
        {
            var r = await limiter.CheckAsync("client1", 3, TimeSpan.FromMinutes(1));
            Assert.True(r.IsAllowed);
        }

        var blocked = await limiter.CheckAsync("client1", 3, TimeSpan.FromMinutes(1));
        Assert.False(blocked.IsAllowed);
    }
}
// BenchmarkDotNet ile client-side performans ölçümü
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
[SimpleJob(iterationCount: 20)]
public class RedisBenchmarks
{
    private IDatabase _db = null!;
    private IConnectionMultiplexer _mux = null!;

    [GlobalSetup]
    public void Setup()
    {
        _mux = ConnectionMultiplexer.Connect("localhost:6379");
        _db = _mux.GetDatabase();
        _db.StringSet("bench:key", new string('x', 256));
    }

    [Benchmark(Baseline = true)]
    public async Task<RedisValue> StringGet()
        => await _db.StringGetAsync("bench:key");

    [Benchmark]
    public async Task StringSet()
        => await _db.StringSetAsync("bench:key", "value");

    [Benchmark]
    public async Task PipelinedGet10()
    {
        var tasks = Enumerable.Range(0, 10)
            .Select(_ => _db.StringGetAsync("bench:key")).ToArray();
        await Task.WhenAll(tasks);
    }

    [Benchmark]
    public void FireAndForget()
        => _db.StringIncrement("bench:counter", flags: CommandFlags.FireAndForget);

    [GlobalCleanup]
    public void Cleanup() => _mux.Dispose();
}

// Çalıştır: dotnet run -c Release -- --filter *RedisBenchmarks*

Test kuralları: Her test kendi key namespace'ini kullan (test:{testId}:*). Test sonunda FLUSHDB veya key cleanup. Production Redis'e asla test bağlantısı açma.

WebApplicationFactory + Testcontainers (End-to-End)

// NuGet: Microsoft.AspNetCore.Mvc.Testing + Testcontainers.Redis + xunit
public class ApiIntegrationTests : IAsyncLifetime
{
    private readonly RedisContainer _redis = new RedisBuilder()
        .WithImage("redis:8-alpine")
        .Build();

    private WebApplicationFactory<Program> _factory = null!;
    private HttpClient _client = null!;

    public async Task InitializeAsync()
    {
        await _redis.StartAsync();

        _factory = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(builder =>
            {
                builder.ConfigureServices(services =>
                {
                    // Production Redis bağlantısını test container ile değiştir
                    services.Remove(services.Single(
                        d => d.ServiceType == typeof(IConnectionMultiplexer)));

                    services.AddSingleton<IConnectionMultiplexer>(
                        ConnectionMultiplexer.Connect(_redis.GetConnectionString()));
                });
            });

        _client = _factory.CreateClient();
    }

    public async Task DisposeAsync()
    {
        _client.Dispose();
        await _factory.DisposeAsync();
        await _redis.DisposeAsync();
    }

    [Fact]
    public async Task GetProduct_Cached_ReturnsFromRedis()
    {
        // İlk istek — DB'den okur, cache'e yazar
        var response1 = await _client.GetAsync("/api/products/1");
        response1.EnsureSuccessStatusCode();

        // İkinci istek — cache'den dönmeli (daha hızlı)
        var sw = Stopwatch.StartNew();
        var response2 = await _client.GetAsync("/api/products/1");
        sw.Stop();

        response2.EnsureSuccessStatusCode();
        var content = await response2.Content.ReadAsStringAsync();
        Assert.Contains(""id":1", content);

        // Cache'den geldiğini doğrula: Redis'te key var mı?
        var mux = _factory.Services.GetRequiredService<IConnectionMultiplexer>();
        var db = mux.GetDatabase();
        var cached = await db.StringGetAsync("product:1");
        Assert.True(cached.HasValue);
    }

    [Fact]
    public async Task RateLimit_ExceedsLimit_Returns429()
    {
        // Limit: 5 requests/minute
        for (int i = 0; i < 5; i++)
        {
            var r = await _client.GetAsync("/api/limited-endpoint");
            Assert.Equal(HttpStatusCode.OK, r.StatusCode);
        }

        var blocked = await _client.GetAsync("/api/limited-endpoint");
        Assert.Equal(HttpStatusCode.TooManyRequests, blocked.StatusCode);
    }
}

CI/CD'de: Testcontainers Docker gerektirir. GitHub Actions'da services: docker aktif olmalı. Container startup ~2-3s, test suite'in toplam süresine minimal etki eder.