2
0

RedisCacheTest.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. <?php
  2. use App\Facades\RedisCache;
  3. use App\Services\RedisCache as RedisCacheService;
  4. /*
  5. | 第一部分:语义。TestCase 已把门面换成 RedisCacheFake,跑的是 RedisCache
  6. | 本身的 JSON 序列化 / TTL / null 逻辑;TTL 与 null 语义与 Laravel Cache 逐条对齐。
  7. */
  8. test('get 不存在时返回默认值,闭包默认值会被求值', function () {
  9. expect(RedisCache::get('missing'))->toBeNull()
  10. ->and(RedisCache::get('missing', []))->toBe([])
  11. ->and(RedisCache::get('missing', fn () => 'lazy'))->toBe('lazy');
  12. });
  13. test('put 后 get 原样取回数组、字符串、布尔', function () {
  14. RedisCache::put('arr', ['a' => 1, '中' => '文']);
  15. RedisCache::put('str', 'abc');
  16. RedisCache::put('bool', false);
  17. expect(RedisCache::get('arr'))->toBe(['a' => 1, '中' => '文'])
  18. ->and(RedisCache::get('str'))->toBe('abc')
  19. ->and(RedisCache::get('bool'))->toBeFalse();
  20. });
  21. test('JSON 往返:数字保持类型,数字字符串仍是字符串', function () {
  22. RedisCache::put('int', 7);
  23. RedisCache::put('float', 1.0);
  24. RedisCache::put('numeric-string', '007');
  25. expect(RedisCache::get('int'))->toBe(7)
  26. ->and(RedisCache::get('float'))->toBe(1.0)
  27. ->and(RedisCache::get('numeric-string'))->toBe('007');
  28. });
  29. test('对象读回来是关联数组', function () {
  30. RedisCache::put('obj', (object) ['a' => 1, 'nested' => (object) ['b' => 'ā']]);
  31. RedisCache::put('collection', collect([['id' => 1], ['id' => 2]]));
  32. expect(RedisCache::get('obj'))->toBe(['a' => 1, 'nested' => ['b' => 'ā']])
  33. ->and(RedisCache::get('collection'))->toBe([['id' => 1], ['id' => 2]]);
  34. });
  35. test('存不了 JSON 的值写入时抛异常', function () {
  36. RedisCache::put('bad', "\xB1\x31");
  37. })->throws(JsonException::class);
  38. test('put 带 TTL 到期失效', function () {
  39. RedisCache::put('int-ttl', 'x', 10);
  40. RedisCache::put('date-ttl', 'x', now()->addSeconds(10));
  41. RedisCache::put('interval-ttl', 'x', new DateInterval('PT10S'));
  42. $this->travel(11)->seconds();
  43. expect(RedisCache::has('int-ttl'))->toBeFalse()
  44. ->and(RedisCache::has('date-ttl'))->toBeFalse()
  45. ->and(RedisCache::has('interval-ttl'))->toBeFalse();
  46. });
  47. test('put / remember 不带 TTL 时按默认 30 天过期', function () {
  48. RedisCache::put('no-ttl', 'x');
  49. RedisCache::put('null-ttl', 'x', null);
  50. RedisCache::remember('remember-null-ttl', null, fn () => 'x');
  51. $this->travelTo(now()->addDays(30)->subSecond());
  52. expect(RedisCache::has('no-ttl'))->toBeTrue()
  53. ->and(RedisCache::has('null-ttl'))->toBeTrue()
  54. ->and(RedisCache::has('remember-null-ttl'))->toBeTrue();
  55. $this->travel(1)->seconds();
  56. expect(RedisCache::has('no-ttl'))->toBeFalse()
  57. ->and(RedisCache::has('null-ttl'))->toBeFalse()
  58. ->and(RedisCache::has('remember-null-ttl'))->toBeFalse();
  59. });
  60. test('put 的 TTL <= 0 等同 forget', function () {
  61. RedisCache::put('k', 'x');
  62. RedisCache::put('k', 'y', 0);
  63. expect(RedisCache::has('k'))->toBeFalse();
  64. });
  65. test('remember 命中时不执行回调', function () {
  66. RedisCache::put('k', 'cached');
  67. $value = RedisCache::remember('k', 60, fn () => throw new RuntimeException('不该执行'));
  68. expect($value)->toBe('cached');
  69. });
  70. test('remember 回调返回 null 时不算命中,下次重新计算', function () {
  71. $calls = 0;
  72. $callback = function () use (&$calls) {
  73. $calls++;
  74. return null;
  75. };
  76. RedisCache::remember('k', 60, $callback);
  77. RedisCache::remember('k', 60, $callback);
  78. expect($calls)->toBe(2);
  79. });
  80. test('has 对 null 值返回 false', function () {
  81. RedisCache::put('k', null);
  82. expect(RedisCache::has('k'))->toBeFalse();
  83. });
  84. test('forget 返回是否真的删掉了', function () {
  85. RedisCache::put('k', 'x');
  86. expect(RedisCache::forget('k'))->toBeTrue()
  87. ->and(RedisCache::forget('k'))->toBeFalse()
  88. ->and(RedisCache::has('k'))->toBeFalse();
  89. });
  90. test('key 带不带开头的 / 是同一个 key', function () {
  91. RedisCache::put('a/b', 1);
  92. RedisCache::put('/c', 2);
  93. expect(RedisCache::get('/a/b'))->toBe(1)
  94. ->and(RedisCache::get('c'))->toBe(2);
  95. });
  96. test('increment 从 0 开始累加,返回新值', function () {
  97. expect(RedisCache::increment('c'))->toBe(1)
  98. ->and(RedisCache::increment('c', 5))->toBe(6)
  99. ->and(RedisCache::get('c'))->toBe(6);
  100. });
  101. /*
  102. | 第二部分:契约。连真实 Redis Cluster(mint.redis_cluster),证明 key 名带前缀、
  103. | 值是 JSON、整数是纯文本(ai-translate 用 int() 读 /task/{id}/pointer)、TTL 与写值一条命令落地,
  104. | 以及 phpredis 对不存在的 key 返回 false 时被归一成 null。集群不可用时跳过。
  105. */
  106. function redisCacheTestPrefix(): string
  107. {
  108. return 'pest-redis-cache';
  109. }
  110. function rawRedisCluster(): RedisCluster
  111. {
  112. return new RedisCluster(null, [config('mint.redis_cluster.host').':'.config('mint.redis_cluster.port')]);
  113. }
  114. function realRedisAvailable(): bool
  115. {
  116. try {
  117. rawRedisCluster()->get('pest-ping');
  118. return true;
  119. } catch (Throwable) {
  120. return false;
  121. }
  122. }
  123. afterEach(function () {
  124. if (! realRedisAvailable()) {
  125. return;
  126. }
  127. $redis = rawRedisCluster();
  128. foreach (['pointer', 'ttl', 'missing', 'default-ttl'] as $key) {
  129. $redis->del(redisCacheTestPrefix().'/'.$key);
  130. }
  131. });
  132. test('数字以纯文本落在集群里,key 带前缀', function () {
  133. (new RedisCacheService(prefix: redisCacheTestPrefix()))->put('pointer', 7);
  134. expect(rawRedisCluster()->get(redisCacheTestPrefix().'/pointer'))->toBe('7');
  135. })->skip(fn () => ! realRedisAvailable(), '没有可用的 Redis Cluster');
  136. test('带 TTL 写入时 key 带过期时间,读回原值', function () {
  137. $cache = new RedisCacheService(prefix: redisCacheTestPrefix());
  138. $cache->put('ttl', ['a' => 1, 'b' => 'ā'], 60);
  139. expect(rawRedisCluster()->get(redisCacheTestPrefix().'/ttl'))->toBe('{"a":1,"b":"ā"}')
  140. ->and($cache->get('ttl'))->toBe(['a' => 1, 'b' => 'ā'])
  141. ->and(rawRedisCluster()->ttl(redisCacheTestPrefix().'/ttl'))->toBeGreaterThan(0)->toBeLessThanOrEqual(60);
  142. })->skip(fn () => ! realRedisAvailable(), '没有可用的 Redis Cluster');
  143. test('不带 TTL 写入时集群里的 key 带默认 30 天过期', function () {
  144. (new RedisCacheService(prefix: redisCacheTestPrefix()))->put('default-ttl', 'x');
  145. expect(rawRedisCluster()->ttl(redisCacheTestPrefix().'/default-ttl'))
  146. ->toBeGreaterThan(RedisCacheService::$defaultTtl - 10)
  147. ->toBeLessThanOrEqual(RedisCacheService::$defaultTtl);
  148. })->skip(fn () => ! realRedisAvailable(), '没有可用的 Redis Cluster');
  149. test('前缀与 key 之间用 / 隔开,key 自带 / 时不重复', function () {
  150. $cache = new RedisCacheService(prefix: redisCacheTestPrefix());
  151. $cache->put('pointer', 1, 60);
  152. $cache->put('/ttl', 2, 60);
  153. expect(rawRedisCluster()->get(redisCacheTestPrefix().'/pointer'))->toBe('1')
  154. ->and(rawRedisCluster()->get(redisCacheTestPrefix().'/ttl'))->toBe('2')
  155. ->and(rawRedisCluster()->exists(redisCacheTestPrefix().'//ttl'))->toBe(0);
  156. })->skip(fn () => ! realRedisAvailable(), '没有可用的 Redis Cluster');
  157. test('不存在的 key 读到 null / 默认值,forget 返回 false', function () {
  158. $cache = new RedisCacheService(prefix: redisCacheTestPrefix());
  159. expect($cache->get('missing'))->toBeNull()
  160. ->and($cache->get('missing', 'd'))->toBe('d')
  161. ->and($cache->forget('missing'))->toBeFalse();
  162. })->skip(fn () => ! realRedisAvailable(), '没有可用的 Redis Cluster');