Facade单元测试中应替换容器内服务标识绑定的实例而非Facade类本身,如用$container->bind('db', $mockConnection)替代mock Db::class,因Facade仅是静态代理,真实逻辑在容器绑定的服务中。

Facade单元测试中如何替换真实依赖实例
Facade 本身不持有实例,它只是容器服务的静态代理。测试时想替换 Db、Cache、Log 等门面背后的真实对象,关键不是 mock Facade 类(比如 Db::class),而是替换容器里它所指向的服务标识(如 'db')绑定的实例。
为什么直接 Mock Facade 会失败
像 Db::table() 这种调用,最终执行的是容器中 'db' 对应的连接实例方法。如果你用 PHPUnit mock think\facade\Db::class,只会拦截 __callStatic,但无法控制实际被代理的对象——这会让测试脱离真实行为,且容易漏掉连接池、事务、查询日志等中间层逻辑。
- mock
think\facade\Db只是伪造了门面类,createFacade()仍会从容器取真实db实例 - 真正生效的替换点在容器绑定:把
$container->bind('db', $mockConnection) - 若没显式 bind,TP 默认使用
think\db\Connection子类,测试时它可能尝试连真实数据库
在 TestCase 中安全替换 db / cache / http 实例
ThinkPHP 的 testingTestCase 提供了 $this->getContainer() 方法,可在 setUp() 或具体测试方法开头注入模拟实例。注意必须在应用初始化后、首次调用 Facade 前完成替换。
- 先调用
$this->createApplication()(否则容器未初始化) - 用
$container = $this->getContainer()获取当前测试容器实例 - 替换 Db 实例:
$container->bind('db', new MockConnection());,其中MockConnection实现think\db\ConnectionInterface - 替换 Cache 实例:
$container->bind('cache', new MockCacheDriver());,注意think\cache\Driver是抽象基类,mock 需覆盖get()/set()等核心方法 - 避免用
app()->bind(),它操作的是全局容器,可能污染其他测试用例
Cache::store('redis') 这类带参数的门面怎么 mock
Cache::store('redis') 不走 'cache' 标识,而是动态解析 store 名称并实例化驱动。它的底层逻辑在 think\cache\Cache 类中,通过 getHandler($name) 创建或复用驱动实例。因此不能只 bind 'cache',而要拦截该工厂行为。
立即学习“PHP免费学习笔记(深入)”;
- 最稳做法:在测试前用
$container->bind('cache', function () { return new MockCache(); });,让Cache::store()内部调用getHandler()时返回你的 mock 实例 - 或者更彻底地,在测试方法内重写
think\cache\Cache的getHandler方法(需 ReflectionClass 修改 private 属性,不推荐) - 别试图 mock
Cache::store('redis')返回值——它每次调用都可能新建实例,mock 返回值无法覆盖后续所有get()/set() - Redis 切库场景(
$redis->handler->select(2))务必在 mock 实例里也模拟select()行为,否则断言会漏掉 DB 切换逻辑
Facade 测试真正的复杂点在于:你 mock 的不是类,而是容器里的“服务名”;而这个服务名和实际类之间隔着一层 getFacadeClass() 映射,一旦写错字符串或没注册进容器,就会抛出 Identifier "xxx" is not registered —— 这个错误不会在写 mock 时出现,而是在第一次调用门面方法时才触发。



















