super_cache_testing 1.0.1
super_cache_testing: ^1.0.1 copied to clipboard
Test utilities for super_cache. FakeCache and ManualClock for deterministic TTL and eviction tests without Future.delayed. Part of the super_cache family.
// ignore_for_file: avoid_print
import 'dart:async';
import 'package:super_cache/super_cache.dart';
import 'package:super_cache_testing/super_cache_testing.dart';
/// super_cache_testing walkthrough
///
/// Shows how to use FakeCache + ManualClock to write deterministic, instant
/// TTL tests — no Future.delayed, no flaky timing, no sleeps.
///
/// 1. ManualClock basics — advance time and jump to a specific instant
/// 2. FakeCache TTL — put, expire, miss; all without waiting
/// 3. Per-entry TTL override — different lifetimes for different entries
/// 4. Counter assertions — hits, misses, puts, gets
/// 5. reset() — wipe all state between logical test cases
/// 6. getResult() — distinguish CacheHit, CacheStale, CacheMiss
///
/// Run with:
/// dart run example/main.dart
void main() {
_section('1. ManualClock basics');
_demo1Clock();
_section('2. FakeCache TTL — no Future.delayed needed');
_demo2Ttl();
_section('3. Per-entry TTL override');
_demo3PerEntryTtl();
_section('4. Counter assertions');
_demo4Counters();
_section('5. reset() for test isolation');
_demo5Reset();
_section('6. getResult() — hit vs stale vs miss');
_demo6GetResult();
print('\nDone!');
}
// ---------------------------------------------------------------------------
// 1. ManualClock basics
//
// ManualClock starts at a fixed point (DateTime.utc(2024)) so tests have a
// deterministic baseline. Use advance() to jump forward by a Duration, or
// setTime() to teleport to any instant.
// ---------------------------------------------------------------------------
void _demo1Clock() {
final clock = ManualClock();
print('initial : ${clock.now}'); // 2024-01-01 00:00:00.000Z
clock.advance(const Duration(hours: 2, minutes: 30));
print('after +2h30m : ${clock.now}'); // 2024-01-01 02:30:00.000Z
clock.advance(const Duration(days: 3));
print('after +3d : ${clock.now}'); // 2024-01-04 02:30:00.000Z
// Jump to an exact point — useful for date-boundary tests.
clock.setTime(DateTime.utc(2025, 12, 31, 23, 59, 59));
print('after setTime: ${clock.now}'); // 2025-12-31 23:59:59.000Z
}
// ---------------------------------------------------------------------------
// 2. FakeCache TTL — no Future.delayed
//
// FakeCache reads the time from the injected ManualClock instead of
// DateTime.now(). Advancing the clock is instant — no actual waiting.
//
// This pattern makes TTL tests run in microseconds regardless of the TTL value.
// ---------------------------------------------------------------------------
void _demo2Ttl() {
final clock = ManualClock();
final cache = FakeCache<String, String>(clock: clock);
// Pretend we cached an auth token that expires in 15 minutes.
cache.put('auth:token', 'Bearer eyJhbGc...',
ttl: const Duration(minutes: 15));
print('at t=0 → ${cache.get("auth:token")}'); // Bearer eyJhbGc...
// Jump forward 10 minutes — still valid.
clock.advance(const Duration(minutes: 10));
print('at t=10m → ${cache.get("auth:token")}'); // Bearer eyJhbGc...
// Jump forward past expiry.
clock.advance(const Duration(minutes: 6)); // now at t=16m
print('at t=16m → ${cache.get("auth:token")}'); // null — expired
// The expired entry is gone; a fresh put restarts the clock from now.
cache.put('auth:token', 'Bearer newToken...',
ttl: const Duration(minutes: 15));
print('after refresh → ${cache.get("auth:token")}'); // Bearer newToken...
unawaited(cache.dispose());
}
// ---------------------------------------------------------------------------
// 3. Per-entry TTL override
//
// pass ttl directly to put() to give individual entries different lifetimes.
// A short-lived "flash deal" can expire in seconds while a "user profile"
// stays cached for days — both in the same FakeCache instance.
// ---------------------------------------------------------------------------
void _demo3PerEntryTtl() {
final clock = ManualClock();
final cache = FakeCache<String, String>(
clock: clock,
defaultTTL: const Duration(hours: 1), // default for entries with no ttl
);
cache.put('deal:flash', 'SAVE50', ttl: const Duration(minutes: 5));
cache.put('user:profile', 'Alice Smith'); // uses defaultTtl (1 hour)
cache.put('config:theme', 'dark',
ttl: Duration.zero); // Duration.zero = never expires
// After 6 minutes: flash deal is gone, everything else survives.
clock.advance(const Duration(minutes: 6));
print('flash deal → ${cache.get("deal:flash")}'); // null — expired
print('user profile → ${cache.get("user:profile")}'); // Alice Smith ✓
print(
'theme config → ${cache.get("config:theme")}'); // dark ✓ (never expires)
// After 2 hours: profile is gone, theme config still lives.
clock.advance(const Duration(hours: 2));
print('\nafter 2h:');
print('user profile → ${cache.get("user:profile")}'); // null — expired
print('theme config → ${cache.get("config:theme")}'); // dark ✓ (still here)
unawaited(cache.dispose());
}
// ---------------------------------------------------------------------------
// 4. Counter assertions
//
// FakeCache tracks puts and gets so you can assert on interaction counts in
// tests — useful for verifying that your repository layer is not
// over-fetching or bypassing the cache unexpectedly.
// ---------------------------------------------------------------------------
void _demo4Counters() {
final clock = ManualClock();
final cache = FakeCache<String, int>(clock: clock);
// Two puts.
cache.put('score:alice', 9800);
cache.put('score:bob', 4200);
// Three gets: two hits, one miss.
cache.get('score:alice'); // hit
cache.get('score:alice'); // hit (second access)
cache.get('score:charlie'); // miss — key not present
print('puts : ${cache.puts}'); // 2
print('gets : ${cache.gets}'); // 3
print('hits : ${cache.metrics.hits}'); // 2
print('misses : ${cache.metrics.misses}'); // 1
print('hit rate : ${(cache.metrics.hitRate * 100).round()}%'); // 67%
unawaited(cache.dispose());
}
// ---------------------------------------------------------------------------
// 5. reset() for test isolation
//
// Call reset() between logical test cases that share the same FakeCache
// instance. It clears all entries AND zeroes all counters, giving you a
// perfectly clean slate without creating a new object.
// ---------------------------------------------------------------------------
void _demo5Reset() {
final clock = ManualClock();
final cache = FakeCache<String, String>(clock: clock);
// ── Simulated "test A" ───────────────────────────────────────────────────
cache.put('user', 'Alice');
cache.get('user');
cache.get('missing');
print(
'test A — entries: ${cache.metrics.currentEntries}, gets: ${cache.gets}');
// entries: 1, gets: 2
// ── reset() between tests ────────────────────────────────────────────────
cache.reset();
print(
'after reset — entries: ${cache.metrics.currentEntries}, gets: ${cache.gets}');
// entries: 0, gets: 0
// ── Simulated "test B" ───────────────────────────────────────────────────
cache.put('product', 'Widget');
cache.get('product');
print(
'test B — entries: ${cache.metrics.currentEntries}, gets: ${cache.gets}');
// entries: 1, gets: 1
// The ManualClock is NOT reset — reset only affects the cache state.
// If you need a fresh clock too, create a new ManualClock().
unawaited(cache.dispose());
}
// ---------------------------------------------------------------------------
// 6. getResult() — CacheHit / CacheStale / CacheMiss
//
// getResult() returns a sealed CacheResult<V> so you can distinguish:
// CacheHit — value is fresh and present
// CacheStale — value is present but past its TTL (soft expiration mode)
// CacheMiss — key not found or hard-expired
//
// Use Dart's exhaustive switch to handle all cases at compile time.
// ---------------------------------------------------------------------------
void _demo6GetResult() {
final clock = ManualClock();
final cache = FakeCache<String, String>(clock: clock);
cache.put('product:1', 'Mechanical Keyboard',
ttl: const Duration(minutes: 10));
// Fresh hit.
_printResult('product:1 (fresh)', cache.getResult('product:1'));
// Unknown key — miss.
_printResult('product:99 (missing)', cache.getResult('product:99'));
// Expire the entry.
clock.advance(const Duration(minutes: 11));
_printResult('product:1 (expired)', cache.getResult('product:1'));
unawaited(cache.dispose());
}
void _printResult(String label, CacheResult<String> result) {
final description = switch (result) {
CacheHit(:final value) => 'HIT → "$value"',
CacheStale(:final value) => 'STALE → "$value" (show while refreshing)',
CacheMiss() => 'MISS → null',
};
print(' $label : $description');
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
void _section(String title) {
print('\n${'─' * 55}');
print(' $title');
print('─' * 55);
}