在PHP项目开发中,很多业务类会存在依赖其他组件或者继承父类的情况,这类类的单元测试往往因为耦合问题变得复杂,PHPUnit提供了多种机制可以高效完成这类场景的测试。

一、处理带依赖的类的测试
当类依赖外部服务或其他组件时,直接实例化会导致测试耦合度高,无法单独验证当前类的逻辑。首先可以通过依赖注入的方式将依赖作为参数传入,再使用PHPUnit的测试替身模拟依赖行为。
1. 依赖注入改造示例
假设我们有一个用户服务类,依赖数据库操作类:
<?php
// 数据库操作接口
interface DatabaseInterface
{
public function find(int $id): ?array;
}
// 用户服务类,通过构造函数注入数据库依赖
class UserService
{
private DatabaseInterface $database;
public function __construct(DatabaseInterface $database)
{
$this->database = $database;
}
public function getUserInfo(int $userId): ?array
{
$user = $this->database->find($userId);
if (empty($user)) {
return null;
}
// 简单处理用户数据
$user['age'] = $user['age'] ?? 0;
return $user;
}
}2. 使用测试替身模拟依赖
PHPUnit提供了createMock方法创建测试替身,可以模拟依赖的返回值,隔离外部影响:
<?php
use PHPUnit\Framework\TestCase;
class UserServiceTest extends TestCase
{
public function testGetUserInfoReturnsNullWhenUserNotExist()
{
// 创建数据库依赖的测试替身
$databaseMock = $this->createMock(DatabaseInterface::class);
// 模拟find方法返回null
$databaseMock->method('find')
->willReturn(null);
$userService = new UserService($databaseMock);
$result = $userService->getUserInfo(1);
$this->assertNull($result);
}
public function testGetUserInfoReturnsProcessedData()
{
$databaseMock = $this->createMock(DatabaseInterface::class);
// 模拟返回原始用户数据
$databaseMock->method('find')
->willReturn(['id' => 1, 'name' => '张三', 'age' => 25]);
$userService = new UserService($databaseMock);
$result = $userService->getUserInfo(1);
$this->assertEquals(25, $result['age']);
$this->assertEquals('张三', $result['name']);
}
}二、处理带继承的类的测试
当类继承父类时,测试需要同时考虑父类逻辑和子类自身的逻辑,避免重复测试父类已覆盖的内容,同时保证子类重写的方法符合预期。
1. 继承场景示例
假设父类是基础服务类,子类继承后重写了部分方法:
<?php
// 基础服务类
class BaseService
{
protected string $prefix = 'base';
public function getPrefix(): string
{
return $this->prefix;
}
public function formatData(string $data): string
{
return $this->prefix . '_' . $data;
}
}
// 子类继承父类,重写prefix和formatData方法
class SubService extends BaseService
{
protected string $prefix = 'sub';
public function formatData(string $data): string
{
return $this->prefix . '_' . strtoupper($data);
}
}2. 测试继承类的注意事项
测试子类时,不需要重复测试父类已经验证过的、未被子类修改的方法,只需要重点测试子类重写的方法和新增逻辑:
<?php
use PHPUnit\Framework\TestCase;
class SubServiceTest extends TestCase
{
public function testGetPrefixReturnsSub()
{
$subService = new SubService();
// 验证子类重写的属性
$this->assertEquals('sub', $subService->getPrefix());
}
public function testFormatDataReturnsExpectedResult()
{
$subService = new SubService();
// 验证子类重写的方法逻辑
$result = $subService->formatData('test');
$this->assertEquals('sub_TEST', $result);
}
}三、依赖和继承同时存在的测试方案
如果类同时有依赖和继承,需要先通过依赖注入拆分依赖,再针对继承特性设计测试用例,避免测试逻辑混乱。
比如子类继承父类,同时依赖缓存组件,我们可以先模拟缓存依赖,再分别验证父类继承的方法和子类重写的方法:
<?php
interface CacheInterface
{
public function get(string $key): ?string;
}
class SubServiceWithCache extends BaseService
{
private CacheInterface $cache;
public function __construct(CacheInterface $cache)
{
$this->cache = $cache;
}
public function getCachedData(string $key): ?string
{
return $this->cache->get($key);
}
}
class SubServiceWithCacheTest extends TestCase
{
public function testGetCachedData()
{
$cacheMock = $this->createMock(CacheInterface::class);
$cacheMock->method('get')
->with('test_key')
->willReturn('cached_value');
$service = new SubServiceWithCache($cacheMock);
// 验证子类新增的依赖相关方法
$this->assertEquals('cached_value', $service->getCachedData('test_key'));
// 验证继承的父类方法不受影响
$this->assertEquals('base', $service->getPrefix());
}
}四、测试最佳实践
- 优先使用依赖注入而非在类内部硬编码依赖,降低测试耦合度
- 测试替身只模拟当前测试需要的依赖行为,不要过度模拟
- 继承类的测试只覆盖子类新增或修改的逻辑,避免重复测试父类已验证的内容
- 每个测试用例只验证一个逻辑点,保持测试代码的清晰可维护