codeigniter 4测试中应手动实例化控制器并直接传入mock对象,绕过服务容器自动解析;mock类型须严格匹配构造函数类型提示,避免调用parent::__construct()以防触发真实服务初始化。

测试环境里怎么让服务容器注入 mock 对象
CI4 的服务容器在测试时默认走真实注册逻辑,直接 new 控制器会触发真实依赖(比如真连数据库),必须绕过自动解析,手动传入 mock 实例。
- 不依赖容器自动解析:构造函数参数直接传 mock 对象,跳过
Services容器调用 - 避免调用
parent::__construct()以外的初始化逻辑:否则可能触发真实服务加载(如 session 启动) - mock 类型必须严格匹配构造函数中的类型提示,否则 PHP 会报
TypeError
PHPUnit 中手动构造控制器并注入 mock
核心是绕过框架的自动实例化流程,自己 new 控制器,并把 mock 对象作为参数传进去。
// tests/Controllers/UserControllerTest.php
use CodeIgniter\Test\CIUnitTestCase;
use App\Controllers\UserController;
use Mockery\MockInterface;
use CodeIgniter\Database\ConnectionInterface;
class UserControllerTest extends CIUnitTestCase
{
public function testIndexReturnsUsers()
{
// 创建 mock 数据库连接
$db = $this->createMock(ConnectionInterface::class);
$db->method('table')->willReturn($this->createMock(\CodeIgniter\Database\BaseBuilder::class));
$db->method('get')->willReturn($this->createMock(\CodeIgniter\Database\Query::class));
// 手动构造控制器,传入 mock
$controller = new UserController($db);
// 注意:这里不能用 $this->get() 或路由访问,否则会走完整容器流程
$result = $controller->index();
$this->assertIsString($result);
}
}
- 不要用
$this->get('/user'):它会触发完整请求生命周期,走真实容器注入 - mock 必须实现接口全部方法,哪怕只 stub 用到的几个(
table()、get()) - 如果控制器构造函数还依赖其他服务(如
SessionInterface),也要一并 mock 并传入
如何 mock 接口绑定的服务(比如自定义 UserService)
当服务是通过接口绑定(如 UserServiceInterface → RealUserService)注册的,测试时需替换绑定,但更稳妥的方式仍是手动传参——因为接口绑定依赖容器启动,而单元测试通常不启动完整容器。
- 在
app/Config/Services.php中的绑定对测试无效,除非你显式调用Services::injectMock() - CI4 没有开箱即用的
injectMock()方法,所谓“注入 mock”实际是靠测试时手动覆盖服务工厂闭包,但操作复杂且易出错 - 推荐路径:构造函数参数全显式声明 + 全手动传 mock,不依赖容器自动解析
容易踩的坑:父类构造函数调用和 filter 冲突
控制器里如果写了 parent::__construct(),它内部会尝试加载请求、响应等服务——这些服务在测试环境下可能未初始化,导致 Fatal Error。
- 测试中若保留
parent::__construct(),需提前设置测试上下文(如$this->withRequest()),否则会崩在Request初始化 - 更简单做法:在测试专用的控制器子类中重写
__construct(),只调用必要逻辑,或干脆不调用parent::__construct()(只要你不访问$this->request等属性) - 别在构造函数里调用
filter相关逻辑:filter 是路由层机制,测试控制器方法时 filter 不生效,硬写会抛异常











