ThinkPHP 8.x 默认未集成 PHPUnit,需手动安装并配置 phpunit.xml;数据库测试必须 Mock Db 门面而非 Connection 实例;Model 测试应通过容器获取实例以保障初始化逻辑;tp:unit 不支持 --filter,须直接调用 vendor/bin/phpunit。

ThinkPHP 8.x 默认不带 PHPUnit,得自己装
ThinkPHP 官方骨架默认没集成 phpunit,连 phpunit.xml 都没有,直接跑 vendor/bin/phpunit 会报错“command not found”。这不是配置问题,是压根没装。
实操建议:
- 用 Composer 安装最新稳定版:运行
composer require --dev phpunit/phpunit ^10(TP8 基于 PHP 8.1+,别用 ^9) - 手动加
phpunit.xml到项目根目录,至少包含<testsuites>和<source>路径映射,否则TestCase类找不到 - TP 的自动加载靠
composer.json的"autoload-dev",确认里面已包含"tests/": "tests/"
数据库测试不能真连 MySQL,得 Mock Db::class
写测试时如果每次跑都连真实数据库,CI 会失败、本地环境可能脏数据、速度慢——这不是风格问题,是可靠性问题。ThinkPHP 的 Db 是门面类,不能直接 new,必须用容器或门面代理。
常见错误现象:Call to undefined method think\db\Connection::table(),因为 Mock 了连接但没处理门面静态调用链。
立即学习“PHP免费学习笔记(深入)”;
实操建议:
- 用
Mockery::mock('alias:think\Db')替换门面绑定,再在setUp()里重绑定:$this->app->bind('think\Db', $mockDb) - 别 Mock
think\db\Connection实例本身,TP8 内部会根据查询类型(query / execute)动态调用不同方法,Mock 粒度太细容易漏 - 对单条 SQL 返回值,用
shouldReceive('select')->andReturn([['id' => 1, 'name' => 'test']]);需要空结果就andReturn([])
测试 Model 方法时,别绕过初始化逻辑
ThinkPHP 的 Model 类依赖容器注入和初始化钩子(比如 initialize()),直接 new User() 会导致 $this->db 为 null 或字段映射失效。
使用场景:测一个自定义的 getUserByTag() 方法,内部用了 $this->where()->find()。
实操建议:
- 从容器获取实例:
$user = $this->app->make(User::class),确保生命周期完整 - 若需控制数据库行为,先 Mock
Db,再通过容器注入到 Model(TP8 支持构造器注入或 setter 注入) - 避免在测试里调
User::where()->find()—— 这会跳过 Model 实例,失去对hidden、append、getAttr等逻辑的覆盖
tp:unit 命令不支持 --filter,得靠 PHPUnit 原生命令
ThinkPHP 提供的 php think test 是个轻量 wrapper,不透传参数,比如你想只跑某个测试方法:php think test --filter testFindUserById 会静默忽略 filter。
性能影响:全量跑所有测试在 CI 上可能超时,尤其带 I/O 模拟的用例;本地调试时反复跑无关用例也浪费时间。
实操建议:
- 直接用
vendor/bin/phpunit --filter testFindUserById - 给测试方法加
@group user注解,然后用--group user批量执行 - CI 中建议用
--exclude-group integration把耗时操作隔离,避免阻塞主流程
Mock 数据库不是为了图省事,而是守住「测试可重复」这条线。一旦发现某个测试在 CI 成功、本地失败,八成是忘了重置 Mock 状态或共享了静态连接实例。










