super_cache_testing 1.0.1 copy "super_cache_testing: ^1.0.1" to clipboard
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.

example/main.dart

// 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);
}
1
likes
160
points
99
downloads

Publisher

verified publisherjihedmrouki.com

Weekly Downloads

Test utilities for super_cache. FakeCache and ManualClock for deterministic TTL and eviction tests without Future.delayed. Part of the super_cache family.

Repository (GitHub)
View/report issues

Topics

#cache #testing #mock

Documentation

API reference

License

MIT (license)

Dependencies

super_cache

More

Packages that depend on super_cache_testing