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

如何Mock嵌套函数中的数据库调用?PHP代码测试求助

如何在不调用数据库的情况下测试包含嵌套数据库操作的PHP方法

问题描述

我正在为自己的代码编写测试,遇到了一个问题:需要测试prepareBundleProducts方法,但它调用的私有方法determinateQuantity里直接操作了数据库,我想在不实际连接数据库的情况下完成测试,但不知道怎么mock嵌套函数里的数据库调用。另外我从没写过测试,不确定要不要重构代码来提升可测试性,也不清楚怎么构建易于测试的代码,希望得到帮助。

原代码

prepareBundleProducts方法

public function prepareBundleProducts(object $bundle): array
{
    $commonValue = fn ($identValue, $quantity) => [
        'productIdent' => [
            'productIdentType' => 'id',
            'identValue' => $identValue
        ],
        'addType' => 'SelectedSizes',
        'quantity' => $quantity
    ];

    return [
        $commonValue(strval($bundle->wheelsData->rimId), $this->determinateQuantity($bundle->wheelsData->rimId)),
        $commonValue(strval($bundle->wheelsData->tireId), 4.0),
        $commonValue(strval($bundle->wheelsData->wrapperId), 4.0),
        $commonValue('79193', 1.0),
    ];
}

determinateQuantity方法

private function determinateQuantity($rimId): float
{
    return match (Products::where('id', $rimId)->pluck('unit')->first()) {
        'sztuka' => 4.0,
        'kpl.' => 1.0,
        default => throw new Exception('Nieznana jednostka miary dla felgi o ID: ' . $rimId . ' w bazie
         danych. Dopuszczone wartości: sztuka (4.0), kpl.(1.0). Sprawdzić to.'),
    };
}

解决方案

一、重构代码提升可测试性(推荐)

核心思路是把数据库依赖从私有方法中剥离,通过依赖注入传入数据查询逻辑,让测试时可以轻松替换为模拟实现。

步骤1:抽离数据库操作到Repository类

创建专门的Repository封装数据库查询,把和数据库相关的逻辑集中管理:

class ProductsRepository
{
    public function getUnitById($rimId): ?string
    {
        return Products::where('id', $rimId)->pluck('unit')->first();
    }
}

步骤2:修改原类注入依赖

调整原服务类,通过构造函数注入ProductsRepository,并修改determinateQuantity的访问权限(改为protected方便测试时mock):

class YourBundleService
{
    private $productsRepository;

    public function __construct(ProductsRepository $productsRepository)
    {
        $this->productsRepository = $productsRepository;
    }

    public function prepareBundleProducts(object $bundle): array
    {
        $commonValue = fn ($identValue, $quantity) => [
            'productIdent' => [
                'productIdentType' => 'id',
                'identValue' => $identValue
            ],
            'addType' => 'SelectedSizes',
            'quantity' => $quantity
        ];

        return [
            $commonValue(strval($bundle->wheelsData->rimId), $this->determinateQuantity($bundle->wheelsData->rimId)),
            $commonValue(strval($bundle->wheelsData->tireId), 4.0),
            $commonValue(strval($bundle->wheelsData->wrapperId), 4.0),
            $commonValue('79193', 1.0),
        ];
    }

    protected function determinateQuantity($rimId): float
    {
        $unit = $this->productsRepository->getUnitById($rimId);
        
        return match ($unit) {
            'sztuka' => 4.0,
            'kpl.' => 1.0,
            default => throw new Exception('Nieznana jednostka miary dla felgi o ID: ' . $rimId . ' w bazie danych. Dopuszczone wartości: sztuka (4.0), kpl.(1.0). Sprawdzić to.'),
        };
    }
}

步骤3:编写测试用例(PHPUnit + Mockery)

现在可以完全脱离数据库,mockProductsRepository来测试各种场景:

use PHPUnit\Framework\TestCase;
use Mockery;

class YourBundleServiceTest extends TestCase
{
    // 测试rim单位为'sztuka'时返回数量4.0
    public function testPrepareBundleProductsWithSztukaUnit()
    {
        // 1. Mock ProductsRepository,指定调用getUnitById时返回'sztuka'
        $repoMock = Mockery::mock(ProductsRepository::class);
        $repoMock->shouldReceive('getUnitById')
            ->with('rim_123')
            ->andReturn('sztuka');

        // 2. 实例化服务,传入mock的Repository
        $service = new YourBundleService($repoMock);

        // 3. 构造测试用的bundle对象
        $bundle = (object)[
            'wheelsData' => (object)[
                'rimId' => 'rim_123',
                'tireId' => 'tire_456',
                'wrapperId' => 'wrap_789'
            ]
        ];

        // 4. 调用方法并断言结果
        $result = $service->prepareBundleProducts($bundle);

        $this->assertEquals(4.0, $result[0]['quantity']);
        $this->assertEquals('rim_123', $result[0]['productIdent']['identValue']);
        $this->assertEquals(4.0, $result[1]['quantity']);
    }

    // 测试未知单位时抛出异常
    public function testPrepareBundleProductsThrowsExceptionForUnknownUnit()
    {
        $this->expectException(Exception::class);
        $this->expectExceptionMessage('Nieznana jednostka miary dla felgi o ID: rim_999 w bazie danych. Dopuszczone wartości: sztuka (4.0), kpl.(1.0). Sprawdzić to.');

        $repoMock = Mockery::mock(ProductsRepository::class);
        $repoMock->shouldReceive('getUnitById')
            ->with('rim_999')
            ->andReturn('unknown_unit');

        $service = new YourBundleService($repoMock);

        $bundle = (object)[
            'wheelsData' => (object)[
                'rimId' => 'rim_999',
                'tireId' => 'tire_456',
                'wrapperId' => 'wrap_789'
            ]
        ];

        $service->prepareBundleProducts($bundle);
    }

    // 清理Mockery的mock实例
    protected function tearDown(): void
    {
        Mockery::close();
    }
}

二、不重构的临时解决方案(不推荐)

如果暂时不想重构代码,可以直接mock Eloquent模型的静态方法,但这种方式耦合度高,容易污染测试环境:

public function testPrepareBundleProductsWithoutRefactoring()
{
    // Mock Products模型的查询链
    $queryMock = Mockery::mock();
    $queryMock->shouldReceive('pluck')->with('unit')->andReturn(collect(['sztuka']));
    
    // 重载Products类,mock where方法
    $productsMock = Mockery::mock('overload:' . Products::class);
    $productsMock->shouldReceive('where')->with('id', 'rim_123')->andReturn($queryMock);

    $service = new YourBundleService();

    $bundle = (object)[
        'wheelsData' => (object)[
            'rimId' => 'rim_123',
            'tireId' => 'tire_456',
            'wrapperId' => 'wrap_789'
        ]
    ];

    $result = $service->prepareBundleProducts($bundle);

    $this->assertEquals(4.0, $result[0]['quantity']);
}

构建可测试代码的核心原则

  • 依赖注入优先:不要在方法内部直接硬编码依赖(比如数据库模型),通过构造函数或方法参数传入,方便替换为mock实现。
  • 单一职责:每个方法/类只负责一件事,比如Repository专门处理数据库查询,服务类专注于业务逻辑。
  • 避免静态方法调用:静态方法会增加代码耦合度,难以mock,尽量用实例方法替代。
  • 合理设置访问权限:如果方法需要被测试或扩展,优先使用protected而非private,或者把逻辑抽成独立类。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 10:36:04