TP5.1 如何编写单元测试:PHPUnit 模拟数据库与 Mock 对象 测试

作者:袖梨 2026-07-28
ThinkPHP5.1单元测试需解耦静态调用,将Db/Model操作封装为可注入的Repository/Service类,用Mockery部分mock或PHPUnit mock自定义DAO层,禁用Db::/Model::静态调用,避免直接mock Db类,并慎用SQLite内存库。

thinkphp5.1 项目做单元测试,不能直接用 phpunit 跑原生 TP 代码——因为 TP5.1 的自动加载、数据库连接、配置加载等机制和标准 PHPUnit 测试环境不兼容。必须手动桥接或重构依赖注入方式,否则 Db::table()Model::get() 这类静态调用会直接连真实库,测试不可控、不可重复。


TP5.1 单元测试前必须解耦数据库静态调用

TP5.1 默认大量使用 DbModel 静态方法,这导致无法被 PHPUnit Mock。你不能对 Db::name('user')->where(...)->select() 做 mock,因为它是静态调用,PHPUnit 的 $this->createMock() 只能 mock 实例对象。

  • 把所有数据库操作封装进 Repository 或 Service 类,并通过构造函数/Setter 注入依赖(例如传入 Db 实例或自定义的 QueryInterface
  • 禁用 Db::Model:: 静态调用,改用 $this->db$this->userModel 这样的实例属性
  • 确保业务逻辑类不 new 任何 TP 核心类,全部走依赖注入(哪怕只是传一个 PDOConnection 对象)

createMock() 替换 DAO 层,而非模拟 Db

别试图 mock Db 类本身——它不是普通类,很多方法是静态的、final 的,mock 失败率高。正确做法是 mock 你自己的数据访问层。

  • 假设你写了 UserRepository,它内部用 $this->db 执行查询;测试时用 $mockRepo = $this->createMock(UserRepository::class)
  • expects()->method('findByName')->willReturn(['id' => 1, 'name' => 'Alice']) 控制返回值
  • 若需验证调用参数,加 with($this->equalTo('admin')),避免因字符串空格或类型隐式转换导致断言失败
  • 注意:mock 对象默认不继承原类方法签名,如果原方法有 type hint(如 array $where),mock 返回值也得匹配,否则运行时报 Fatal error

TP5.1 中 SQLite 内存库不推荐用于单元测试

sqlite::memory: 看起来干净,但在 TP5.1 里实际踩坑多:配置复杂、事务行为和 MySQL 不一致、外键默认关闭、Db 类对内存库的适配不完整(比如某些聚合函数报错)。

立即学习“PHP免费学习笔记(深入)”;

  • TP5.1 的 database.php 不支持直接设 'dsn' => 'sqlite::memory:',需额外 patch thinkdbconnectorSqlite
  • 即使跑通,Db::table('user')->insert() 后再 select 可能查不到数据——因为 TP5.1 的连接复用机制在内存库下容易丢上下文
  • 真正需要 SQL 行为验证(比如 join、group by)时,才考虑用 RefreshDatabase + 真实 SQLite 文件(非内存),并配合 setUp() 清库

Mockery 比 PHPUnit 原生 Mock 更适合 TP5.1 场景

TP5.1 的类常含复杂构造逻辑(如自动读配置、初始化连接),PHPUnit 的 createMock() 创建空对象后,很多方法调用直接 fatal。Mockery 支持更灵活的“伪造”(fake)和部分 mock。

  • 安装:composer require --dev mockery/mockery
  • 写法示例:$mockDb = Mockery::mock('thinkdbQuery')->makePartial(),然后 shouldReceive('select')->andReturn([['id'=>1]])
  • 关键点:用 makePartial() 保留原方法骨架,只 mock 想替换的部分;否则 select() 调用时可能因未 mock parseWhere() 等内部方法而崩溃
  • 测试结束必须调用 Mockery::close()(通常放 tearDown()),否则后续测试可能受残留 mock 影响

TP5.1 的单元测试难点不在工具链,而在 如何让框架代码“可测” ——静态调用、全局状态、隐式单例这三座山,不推倒就永远卡在“跑不起来”或“测了等于没测”。mock 对象不是银弹,它只对松耦合代码生效。

相关文章

精彩推荐