You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHPUnit中如何将Mock Database对象注入到Mock ParticipantView对象?

问题:如何为测试注入带Mock数据库的ParticipantView对象?

我想测试BuildParticipant类中checkCredentials()方法的输出,先看类结构:

Class BuildParticipant
{
    public function __construct(DBConnectionWrapper $db, ?Logger $logger=null)
    {
        $this->participant = new ParticipantView($db);
        // 省略其他代码
    }

    public function build($participantId)
    {
       $credentialsOK = $this->checkCredentials($participantId, $this->participant);
       // 省略其他代码
    }
 
    protected function checkCredentials($participantId, Crud $participant): bool
    {
        $data = $participant->find($participantId)->nextRecord();
        // 基于$data做逻辑处理并返回布尔值
    }
}

原来的checkCredentials写法可测试性很差:

protected function checkCredentials($participantId): bool
    {
        $data = $this->participant->find($participantId)->nextRecord();
    }

我改成把ParticipantView注入方法后,理论上可以MockParticipantView::find(),但构建Mock时遇到了问题:

测试代码:

<?php
use PHPUnit\Framework\TestCase;

class buildNewParticipantTest extends TestCase {
    public function testCredentialInclusion()
    {
        $mockDb = $this->getMockBuilder('Database')->getMock();

        // 尝试修复测试错误但失败(ParticipantView是CrudView的子类)
        $participantView = new ParticipantView($mockDb);

        $pv = $this
                ->getMockBuilder($participantView::class)
                ->onlyMethods(['find'])
                ->getMock();

        $pv
            ->expects($this->once())
            ->method('find')
            ->willReturn([
                'id'        => '1234',
                'ssn'       => '555-12-3456',
                'lastName'  => 'Smith',
                'firstName' => 'John',
                'credentials' => 'foo' 
            ]);

        $this->assertEquals('John', $pv['firstName']);
    }
}

运行测试后报错:

There was 1 error:

1) buildNewParticipantTest::testCredentialInclusion
ArgumentCountError: Too few arguments to function CrudView::__construct(), 0 passed

补充类结构信息:
所有对象都是用于从数据库获取数据(无展示逻辑):

abstract class DBConnectionWrapper
{
    // 类内容
}
class DB_ODBC extends DBConnectionWrapper
{
    // 类内容
}
class Database extends DB_ODBC 
{
    // 类内容
}
class Crud implements CrudDefinition
{
    public function __construct(DBConnectionWrapper $db, ?Monolog\Logger $log = null)
    {
        // 构造方法内容
    }
}
/**
 * CRUD View
 *
 * A CrudView accesses a query as if it were a table, so that we can
 * combine several tables into a single table and use Crud methods on
 * it.
 *
 * Read Only!
 */
class CrudView extends Crud
{
    public function __construct(DBConnectionWrapper $db)
    {
        parent::__construct($db);
        // 其他处理逻辑...
    }
}
class ParticipantView extends CrudView 
{
    // 类内容
}

正常实例化ParticipantView的方式:

$db = new Database();
$pv = new ParticipantView($db);

我该如何将Mock Database对象注入到Mock ParticipantView对象中?


解决方案

问题出在你构建Mock的方式上:getMockBuilder($participantView::class)会直接尝试实例化ParticipantView类,但没有传入构造方法必需的DBConnectionWrapper参数,所以触发了参数不足的错误。

正确的做法是直接针对ParticipantView类构建Mock,同时通过setConstructorArgs()传入Mock的数据库对象:

修正后的测试代码:

<?php
use PHPUnit\Framework\TestCase;

class buildNewParticipantTest extends TestCase {
    public function testCredentialInclusion()
    {
        // 1. 构建Mock数据库对象
        $mockDb = $this->getMockBuilder(Database::class)->getMock();

        // 2. 构建ParticipantView的Mock,传入构造参数
        $pv = $this
                ->getMockBuilder(ParticipantView::class)
                ->onlyMethods(['find'])
                ->setConstructorArgs([$mockDb]) // 传入Mock的DB对象
                ->getMock();

        // 3. 处理链式调用:Mock find返回带nextRecord方法的对象
        $mockRecord = $this->getMockBuilder(stdClass::class)
                           ->addMethods(['nextRecord'])
                           ->getMock();
        $mockRecord->method('nextRecord')
                   ->willReturn([
                        'id'        => '1234',
                        'ssn'       => '555-12-3456',
                        'lastName'  => 'Smith',
                        'firstName' => 'John',
                        'credentials' => 'foo' 
                    ]);

        $pv->expects($this->once())
           ->method('find')
           ->with('1234') // 指定预期参数,增强测试严谨性
           ->willReturn($mockRecord);

        // 4. 测试BuildParticipant的checkCredentials方法
        $buildParticipant = new BuildParticipant($mockDb);
        // 反射调用protected方法
        $reflection = new ReflectionClass($buildParticipant);
        $method = $reflection->getMethod('checkCredentials');
        $method->setAccessible(true);
        $result = $method->invoke($buildParticipant, '1234', $pv);

        // 根据业务逻辑断言结果,比如credentials为'foo'时返回true
        $this->assertTrue($result);
    }
}

关键说明:

  1. 构造参数传递:用setConstructorArgs()把Mock数据库对象传入ParticipantView的Mock,解决构造方法参数不足的问题。
  2. 链式调用Mock:原代码中find()->nextRecord()是链式调用,需要先让find返回一个带nextRecord方法的Mock对象,再让该方法返回测试数据。
  3. 访问protected方法:测试BuildParticipant中的checkCredentials(protected方法)时,需要用PHP反射机制将方法设为可访问后再调用。

另外,你原测试中的$this->assertEquals('John', $pv['firstName']);是错误的——$pv是Mock对象,不是数组,不能用数组下标访问属性,应该通过模拟方法返回的数据来断言业务逻辑结果。


内容的提问来源于stack exchange,提问作者Tim Morton

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 00:04:49