如何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
相关产品推荐
相关产品推荐

