service类必须通过容器获取,不可直接new;测试需继承testcase并用$this->app->make()实例化,依赖门面用fake()、外部服务用mock(),数据库用sqlite内存库隔离。

Service类不能直接 new,必须走容器获取
ThinkPHP 的 Service 层通常依赖框架容器注入(如数据库、缓存、其他 Service),直接 new UserService() 会导致 $this->db 为 null、Db::name() 报错,或无法触发事件监听。测试时必须通过容器实例化,否则连构造函数里的依赖都加载失败。
正确做法是继承 think\testing\TestCase,并在测试方法中用 $this->app->make(UserService::class) 获取实例。这个 $this->app 是测试专用的已初始化容器,能完整加载配置、中间件和绑定关系。
- 不要写
$service = new UserService()—— 容器未参与,所有protected $db或protected $cache都是 null - 若 Service 构造函数有非容器参数(如
string $prefix),需在make()时传入:$this->app->make(UserService::class, ['prefix' => 'test_']) - 确保
tests/TestCase.php中的createApplication()返回了正确的启动入口,否则$this->app不可用
数据库操作必须隔离,别动生产表
Service 方法常含 Db::table()->insert() 或 UserModel::create(),直接跑测试会污染数据。ThinkPHP 测试不推荐用事务回滚(Db::startTrans())来“假装隔离”,因为事务无法跨请求生效,且模型事件、钩子可能已触发。
真正可靠的方案是换库:在 phpunit.xml 中强制指定测试环境使用 SQLite 内存数据库,或清空+重建测试专用 MySQL 表。
- 推荐内存 SQLite:在
phpunit.xml的<php></php>节点里加<env name="DB_TYPE" value="sqlite"></env><env name="DB_DATABASE" value=":memory:"></env> - 若必须用 MySQL,请在
setUp()中执行Db::execute('TRUNCATE TABLE user'),但注意外键约束和自增 ID 重置问题 - 避免在 Service 测试里调用真实第三方 API;用
$this->mock(ThirdPartyClient::class)替代,否则测试变集成测试,不稳定且慢
Mock 外部依赖要 mock 到具体方法,不是类名
Service 往往调用缓存、消息队列、HTTP 客户端等外部服务。PHPUnit 的 createMock() 只能拦截对象方法调用,但 ThinkPHP 常用门面(Facade)如 Cache::get()、Http::get() 是静态调用,mock 类名没用 —— 它根本不会走你 mock 的实例。
正确方式分两类:
- 对门面:用
Cache::fake()或Http::fake()(ThinkPHP 6.1+ 自带),它们会接管所有静态调用并返回预设值 - 对普通类依赖(如
SmsService):先在 Service 的构造函数中声明类型提示,再用$mock = $this->mock(SmsService::class); $mock->shouldReceive('send')->andReturn(true);(需配合orchestra/testbench或手动绑定) - 别 mock
Db或Model本身 —— 它们已在 SQLite 内存库中隔离,mock 反而掩盖 SQL 错误
测试命名和断言要对应 Service 行为,不是 HTTP 状态
Service 层测试不是控制器测试,不关心 200 或 json 格式。它的输入是参数数组或对象,输出是布尔、数组、模型实例或抛出异常。断言必须落在这些真实返回值上。
- 错误示范:
$this->assertEquals(200, $response->getStatusCode())—— 这是控制器层逻辑 - 正确示范:
$this->assertTrue($service->activateUser(123))或$this->assertInstanceOf(User::class, $service->findActiveUser(123)) - 异常测试用
$this->expectException(UserNotFoundException::class),而不是检查响应体是否含 "not found" - 如果 Service 方法返回数组(如
['code' => 0, 'msg' => 'ok']),直接 assert key 和 value,别转成 JSON 再 decode
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











