CodeIgniter 4 测试中 Mock 系统类:用 mock() 与 MockCache 隔离缓存依赖的完整实践

发布时间:2026/10/12 1:45:21
CodeIgniter 4 测试中 Mock 系统类:用 mock() 与 MockCache 隔离缓存依赖的完整实践 后端Web框架【免费下载链接】CodeIgniter4Open Source PHP Framework (originally from EllisLab)项目地址https://gitcode.com/gh_mirrors/co/CodeIgniter4点击查看免费下载导读CodeIgniter 4 框架内置了一套面向测试的 Mock 机制多个核心组件缓存、邮件、Session、HTTP 请求等都提供了可替换的 Mock 类让你在运行测试时无需触碰真实的缓存存储、不必真正发送邮件同时还能对某个操作是否发生做出断言。本文以缓存Cache为主线深入讲解mock()辅助函数、MockCache的注入原理、bypass()与clean()的隔离手段、三个开箱即用的缓存断言方法并结合仓库源码与测试用例给出可直接复用的实战写法。读完你即可在自己的测试中稳定替换缓存服务并验证数据是否被正确缓存。Mock 系统类框架为测试准备的替身组件框架内的多个组件在测试时提供了可替换的 Mock 版本它们可以在测试执行期间顶替正常类并额外提供一系列断言用来验证某个动作是否发生了或没有发生——例如数据是否被正确缓存、邮件是否按预期发出等。这是官方文档 mocking.rst 对这一机制的总体描述。这些 Mock 类统一存放在 system/Test/Mock 目录下包括MockCache内存版缓存处理器测试默认使用防止读写真实缓存文件或 Redis/MemcachedMockEmail模拟邮件发送确保测试不会真的发出邮件MockSession基于ArrayHandler的内存 SessionMockCURLRequest、MockIncomingRequest、MockResponseHTTP 相关请求/响应替身MockConnection、MockBuilder、MockQuery、MockResult数据库查询链路替身MockEvents、MockLogger、MockFileLogger、MockLanguage、MockSecurity等其余基础设施组件。它们之所以能被无缝替换关键在于统一入口——mock()辅助函数。mock() 函数Mock 的统一入口与注入原理mock()定义在 system/Helpers/test_helper.php签名如下function mock(string $className): object以缓存为例官方文档给出的最小用法是$mock mock(\CodeIgniter\Cache\CacheFactory::class);这一行代码做了三件事对应mock()源码逻辑读取目标类的::$mockClass静态属性确定要实例化的 Mock 类读取::$mockServiceName确定要把 Mock 注入到哪个服务名下实例化 Mock 类后调用Services::injectMock($mockService, $mock)将其注册进服务容器。若$mockClass为空或类不存在会抛出TestException::forInvalidMockClass()。对缓存而言这两处静态属性定义在 system/Cache/CacheFactory.phppublic static $mockClass MockCache::class; public static $mockServiceName cache;因此mock(CacheFactory::class)会返回一个CodeIgniter\Test\Mock\MockCache实例同时把它以cache服务名注入Services。之后你代码里任何service(cache)或Config\Services::cache()调用拿到的都是这个 Mock 实例而不是配置文件中指定的真实缓存处理器。注入的底层实现injectMock()位于 system/Config/BaseService.php其逻辑是同时写入共享实例表与 Mock 表public static function injectMock(string $name, $mock) { $name strtolower($name); static::$instances[$name] $mock; static::$mocks[$name] $mock; }被注入后后续请求该服务时直接返回 Mock 实例不会再去实例化真实处理器。框架在 tests/system/Cache/CacheMockTest.php 中专门验证了这一行为public function testMockReturnsMockCacheClass(): void { $this-assertInstanceOf(BaseHandler::class, service(cache)); $mock mock(CacheFactory::class); $this-assertInstanceOf(MockCache::class, $mock); $this-assertInstanceOf(MockCache::class, service(cache)); }service(cache)在 mock 前后的类型差异直观证明了注入生效。CIUnitTestCase 默认已经帮你 Mock 了缓存如果你继承的是CodeIgniter\Test\CIUnitTestCase那么缓存、邮件、Session 三个服务默认已在setUp()阶段被替换为 Mock。这在 system/Test/CIUnitTestCase.php 的$setUpMethods属性中定义protected $setUpMethods [ resetFactories, mockCache, mockEmail, mockSession, ];其中mockCache()的实现system/Test/CIUnitTestCase.php直接注入了一个全新的MockCacheprotected function mockCache() { Services::injectMock(cache, new MockCache()); }这也解释了官方文档在 testing/overview.rst 中的提醒Cache、Email、Session 服务默认被 Mock 是为了防止侵入性的测试行为避免真的读写缓存、发邮件、污染 Session。如果你不希望某个服务被自动替换可以按需删减$setUpMethods例如protected $setUpMethods [resetFactories, mockCache, mockSession];如果你没有使用CIUnitTestCase例如直接继承 PHPUnit 的TestCase则需要手动调用mock(CacheFactory::class)或Services::injectMock(cache, new MockCache())。多个测试方法间的隔离clean() 与 bypass()MockCache的数据存放在对象内部的数组中protected $cache []因此它在同一测试进程内是有记忆的。官方文档明确建议当同一个测试文件里有多个测试方法时应在setUp()中调用clean()或bypass()确保每个测试从干净状态开始。clean()清空$cache与$expirations两个内部数组让 Mock 回到全新状态见 MockCache.phpbypass()清空数据的同时打开旁路开关让 Mock 从此拒绝一切写入见下节。典型写法protected function setUp(): void { parent::setUp(); $mock mock(\CodeIgniter\Cache\CacheFactory::class); $mock-clean(); // 或 $mock-bypass(); }更进一步的说明见下一节bypass()。附加方法 bypass()让测试彻底不依赖缓存官方文档给出的bypass()用法$mock mock(\CodeIgniter\Cache\CacheFactory::class); // Never cache any items during this test. $mock-bypass();bypass()的实现MockCache.php会先调用clean()清空存量数据再把$bypass开关置为truepublic function bypass(bool $bypass true) { $this-clean(); $this-bypass $bypass; return $this; }开关生效的位置在save()方法MockCache.phppublic function save(string $key, $value, int $ttl 60): bool { if ($this-bypass) { return false; } // ... 正常写入 $this-cache 与 $this-expirations }bypass()等价于让测试假装使用了 Dummy 处理器所有写入请求都被忽略并返回false读取一律 miss。它特别适合不关心缓存内容、只关心业务逻辑本身的测试——避免因为之前测试或外部数据残留而影响结果也避免让测试依赖于缓存是否命中这一不确定状态。三个开箱即用的缓存断言官方文档在 mocking/003.php 中给出了三个断言示例$mock mock(\CodeIgniter\Cache\CacheFactory::class); // Assert that a cached item named $key exists $mock-assertHas($key); // Assert that a cached item named $key exists with a value of $value $mock-assertHasValue($key, $value); // Assert that a cached item named $key does NOT exist $mock-assertMissing($key);这三个方法定义在 MockCache.php均基于 PHPUnit 的Assert门面实现断言方法语义底层实现要点assertHas($key)断言缓存中存在名为$key的条目调用Assert::assertNotNull($this-get($key))失败时提示 The cache does not have an item named: ...。因为false与null都是合法缓存值这里只校验存在性而非值assertHasValue($key, $value)断言$key存在且值严格等于$value先复用assertHas()处理键不存在的情况再用Assert::assertSame()做严格比较assertMissing($key)断言缓存中不存在$key直接检查内部数组Assert::assertArrayNotHasKey($key, $this-cache)需要留意assertHas()与assertMissing()的差异assertHas()通过get()判断null视为不存在即使显式缓存了null也会判为 miss而assertMissing()直接检查数组键。因此当你想缓存null值并验证时assertMissing()的语义更贴近键是否存在。断言在实际测试中的用法框架自带测试 tests/system/Cache/CacheMockTest.php 演示了完整的写入-断言-绕过流程public function testMockCaching(): void { /** var MockCache $mock */ $mock mock(CacheFactory::class); // Ensure it stores the value normally $mock-save(foo, bar); $mock-assertHas(foo); $mock-assertHasValue(foo, bar); // Try it again with bypass on $mock-bypass(); $mock-save(foo, bar); $mock-assertMissing(foo); }这个测试同时验证了三件事默认写入正常、断言可用、bypass()后写入被拒绝。你可以直接把它当作自己测试的样板。MockCache 源码级解析内存缓存背后的完整能力虽然文档重点介绍的是测试辅助方法但MockCache本身实现了完整的CacheInterface见 system/Test/Mock/MockCache.php即它具备真实缓存处理器的全部行为只是数据只存在内存中。除上述方法外还包括remember($key, $ttl, $callback)命中则直接返回否则执行回调并缓存结果MockCache.phpdelete($key)、deleteMatching($pattern)按键删除与按fnmatch通配符批量删除increment($key, $offset)/decrement($key, $offset)原子自增/自减仅对整数值生效非整数返回falsegetCacheInfo()返回当前存储中全部键名array_keys($this-cache)getMetaData($key)返回条目的过期时间戳键不存在或已过期返回nullinitialize()空实现isSupported()恒为true——Mock 永远可用这正是它能替换任何真实处理器、不受环境限制的关键。过期时间记录在$expirations数组中save()时若$ttl 0则记录Time::now()-getTimestamp() $ttl作为绝对过期时间MockCache.php。注意get()并不会主动检查过期过期语义主要由getMetaData()体现这与真实处理器如FileHandler的按需清理策略有所区别——测试中你通常无需关心这一点因为每个测试的 Mock 都是全新的。另外MockCache 对 key 的处理会经过static::validateKey($key, $this-prefix)继承自BaseHandler即与实际处理器一致地应用前缀与非法字符校验保证你在测试中写的 key 规则与生产环境一致。实战在集成测试中验证响应是否被缓存ResponseCacheHTTP 页面缓存是框架内部大量使用 MockCache 的典型场景。在 tests/system/Cache/ResponseCacheTest.php 中可以看到/** var MockCache $mockCache */ $mockCache mock(CacheFactory::class);随后测试通过$mockCache-assertHas(...)、assertHasValue(...)等方法验证一次请求处理后页面缓存是否被正确写入、TTL 是否符合预期。这印证了 Mock 机制的价值被测代码这里是ResponseCache感知不到缓存实现差异仍走service(cache)的完整调用链而测试则能拿到一个可检查、可断言的内存缓存。推广到你的业务代码一个典型的控制器/服务缓存测试长这样use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockCache; use CodeIgniter\Cache\CacheFactory; final class UserServiceTest extends CIUnitTestCase { private MockCache $cache; protected function setUp(): void { parent::setUp(); // 默认已注入 MockCache /** var MockCache $cache */ $cache mock(CacheFactory::class); $cache-clean(); $this-cache $cache; } public function testUserDataIsCached(): void { // 被测代码内部调用 service(cache)-save(user_1, $user, 300); $this-someService-getUser(1); $this-cache-assertHas(user_1); $this-cache-assertHasValue(user_1, [id 1, name Alice]); } public function testCacheBypassed(): void { $this-cache-bypass(); $this-someService-getUser(1); // 即便业务代码尝试写缓存Mock 也已拒绝存储 $this-cache-assertMissing(user_1); } }注意parent::setUp()中默认的mockCache()已注入一次 Mock若在测试方法里再次调用mock(CacheFactory::class)会覆盖同一服务名下的实例所以建议在setUp()中统一完成获取 Mock clean()保持状态一致。测试结束后的清理Services::reset() 与 resetSingle()Mock 注入会一直留在Services容器中直到被显式重置。官方文档在 testing/overview.rst 中提供了两个配套方法Services::reset()移除全部 Mock 与共享实例让Services回到原始状态CIUnitTestCase还提供了等价的$this-resetServices()CIUnitTestCase.php。注意该操作会连带清空RouteCollection的路由如需加载路由要调用Services::routes()-loadRoutes()Services::resetSingle(string $name)只移除单个服务的 Mock 与共享实例BaseService.php适合只想恢复某一个服务时使用。在CIUnitTestCase中大部分状态由框架在setUp/tearDown阶段自动处理你通常不需要手动调用重置但当你使用原生 PHPUnitTestCase编写集成测试、或在测试中途需要换成真实服务时这两个方法就是控制权所在。最佳实践小结默认继承CIUnitTestCase缓存、邮件、Session 已自动 Mock你的测试天然与外部存储/网络隔离单文件多测试方法时先clean()每个测试从空缓存开始避免相互污染不关心缓存命中时用bypass()让测试完全摆脱缓存状态依赖行为可预期断言选型验证有值用assertHasValue()严格相等验证存在用assertHas()验证不存在用assertMissing()key 规则与生产一致MockCache 同样执行validateKey()与前缀逻辑测试中滥用特殊字符的 key 可能与生产行为不符需要真实服务时用Services::resetSingle(cache)或Services::reset()解除 Mock并注意路由表的连带重置。延伸阅读Mock 机制总览与Services::injectMock()/reset() testing/overview.rstmock()辅助函数完整实现 system/Helpers/test_helper.phpMockCache 源码bypass、断言、完整缓存接口 system/Test/Mock/MockCache.phpCacheFactory 的 mock 声明$mockClass/$mockServiceName system/Cache/CacheFactory.php测试用例样板 tests/system/Cache/CacheMockTest.php、tests/system/Cache/ResponseCacheTest.php其余可 Mock 组件清单 system/Test/Mock赞分享后端Web框架【免费下载链接】CodeIgniter4Open Source PHP Framework (originally from EllisLab)项目地址https://gitcode.com/gh_mirrors/co/CodeIgniter4点击查看免费下载相关推荐Mock对象CleanArchitecture测试依赖隔离Mock对象CleanArchitecture测试依赖隔离 引言为什么需要Mock对象 在现代软件开发中测试驱动开发TDD和单元测试已成为保证代码质后端Vint源码解析Python实现的Vim脚本检查原理Vint源码解析Python实现的Vim脚本检查原理 Vint是一款用Python实现的快速且高度可扩展的Vim脚本语言检查工具它能够帮助开发者在编写Vim开发工具Vault-Operator开发指南如何扩展和自定义Operator功能的完整教程Vault Operator开发指南如何扩展和自定义Operator功能的完整教程 Vault Operator是一个强大的Kubernetes Operat上一篇4 个网络工具搞定子网计算与 MAC 地址查询下一篇LinkSwift3分钟搞定八大网盘直链提取的实用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询