ThinkPHP 8 单元测试须手动配置 PHPUnit,移除 think:test 命令;测试类放 tests/ 目录、以 Test 结尾,方法以 test 开头;Model 测试需 Container::get() 获取实例;控制器测试须模拟 Request;数据库测试推荐 :memory: SQLite 并手动建表。

ThinkPHP 8 的单元测试用 phpunit,不是 think:test
ThinkPHP 8 官方不再内置测试命令或专属测试框架,think:test 命令在 TP8 中已移除。你得自己配 phpunit,否则跑不起来任何测试用例。
常见错误现象:php think test 报错“Command not found”,或者执行后无反应、提示“no tests found”——大概率是没装 phpunit 或配置文件缺失。
- 必须手动安装:
composer require --dev phpunit/phpunit ^10.5(TP8 基于 PHP 8.1+,别装 v9) -
phpunit.xml要放在项目根目录,且需包含bootstrap="vendor/autoload.php",否则类自动加载失败 - 测试类必须放在
tests/目录下,命名以Test结尾(如UserServiceTest.php),类名也得是UserServiceTest - 方法名必须以
test开头(如testShouldCreateUser),否则phpunit不识别
测试 Model 时不能直接 new,要用 Container::get()
ThinkPHP 的 Model 依赖容器注入(比如数据库连接、事件监听器),直接 new User() 会导致 Db 未初始化、save() 报 Call to a member function query() on null。
使用场景:你想测一个带关联查询或自动时间戳的 Model 方法。
立即学习“PHP免费学习笔记(深入)”;
- 正确写法:
$user = Container::get(User::class);,这样能拿到完整生命周期的实例 - 如果只是简单数据构造(不涉及 DB 操作),可用
User::make(['name' => 'test']),它绕过容器但保留模型属性处理逻辑 - 注意:
Container::get()在测试中默认不加载应用配置,如需数据库,得在setUp()里手动调用App::init()或显式绑定Connection
测试控制器要模拟 Request,不能直接调用 action 方法
直接 (new IndexController())->index() 会因 $this->request 为 null 报错,而且跳过了中间件、路由参数解析、验证器注入等关键链路。
性能影响:跳过请求模拟看似快,实则测的是“裸函数”,跟真实 HTTP 请求行为偏差极大,CI 环境容易漏掉中间件引发的 bug。
- 用
think\testing\Mocker(TP8 内置)快速构造请求:$this->mockRequest('GET', '/api/user', ['id' => 123]) - 若需更细粒度控制(如 header、cookie),改用
think\facade\Request::bind()+ 手动 newRequest实例并 set 属性 - 别忘了清理:每个测试末尾加
Request::clear();,否则上一个测试的 request 数据可能污染下一个
数据库测试记得用 :memory: SQLite,别连开发库
用 MySQL/PostgreSQL 连开发库做单元测试,轻则变慢、重则清空真实数据。TP8 默认不提供测试专用数据库配置,得自己切。
兼容性影响:SQLite 的 SQL 语法和 MySQL 有差异(比如不支持 JSON_CONTAINS),复杂查询要降级写法或跳过该用例。
- 在
phpunit.xml里设环境变量:<env name="DB_TYPE" value="sqlite"></env>,并在config/database.php中响应切换 - SQLite 内存模式写法:
'database' => ':memory:',每次运行都是干净库,无需 migrate:refresh - 注意:内存数据库不持久,
setUp()里必须重新执行Db::execute()建表语句,或用think-migration的up()方法
最容易被忽略的是迁移回滚顺序和事务嵌套——SQLite 不支持 SAVEPOINT 嵌套,beginTransaction() 套两层就会崩。测 DB 相关逻辑时,老老实实用单层事务 + rollback() 收尾。


















