PHPUnit源码解析
PHPUnit是PHP最流行的单元测试框架,本文深入解读其核心源码和设计思想,帮助你更好地理解测试框架的工作原理。
一、PHPUnit概述
1.1 PHPUnit特点
【PHPUnit核心特点】
1、xUnit架构
- 遵循xUnit测试框架模式
- TestCase基类
- 断言机制
2、丰富的断言
- 40+种断言方法
- 自定义断言
- 约束系统
3、Mock对象
- 测试替身
- 行为验证
- 依赖隔离
4、代码覆盖率
- 行覆盖率
- 分支覆盖率
- 路径覆盖率
1.2 架构设计
【PHPUnit架构图】
┌─────────────────────────────────────────────────────────────┐
│ PHPUnit CLI │
│ (命令行入口) │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ TextUI/Command │
│ (命令解析) │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ TestRunner │
│ (测试运行器) │
└─────────────────────────────┬───────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ TestSuite │ │ TestCase │ │ TestResult │
│ (测试套件) │ │ (测试用例) │ │ (测试结果) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ TestLoader │ │ Assertions │
│ (测试加载) │ │ (断言系统) │
└─────────────────┘ └─────────────────┘
二、TestCase核心源码分析
2.1 TestCase基类结构
<?php
/**
* PHPUnit TestCase源码分析
* 文件位置:phpunit/phpunit/src/Framework/TestCase.php
*/
namespacePHPUnit\Framework;
usePHPUnit\Framework\Constraint\Constraint;
usePHPUnit\Framework\MockObject\MockBuilder;
abstractclassTestCaseextendsAssertimplementsReorderable, SelfDescribing, Test{
/**
* 测试名称
*/
private string $name;
/**
* 测试数据
*/
privatearray $data = [];
/**
* 数据名称
*/
private string $dataName = '';
/**
* 期望的异常
*/
private ?string $expectedException = null;
/**
* 期望的异常消息
*/
private string $expectedExceptionMessage = '';
/**
* 期望的异常代码
*/
privatenull|int|string $expectedExceptionCode = null;
/**
* 测试结果
*/
private ?TestResult $result = null;
/**
* 输出缓冲级别
*/
private int $outputBufferingLevel;
/**
* Mock对象列表
*/
privatearray $mockObjects = [];
/**
* 构造函数
*/
publicfunction__construct(string $name){
$this->name = $name;
}
/**
* 运行测试
* 这是测试执行的核心方法
*/
publicfunctionrun(?TestResult $result = null): TestResult{
// 创建或使用传入的TestResult
$result ??= new TestResult();
// 注册测试开始
$result->startTest($this);
try {
// 执行测试
$this->runTest();
} catch (AssertionFailedError $e) {
// 断言失败
$result->addFailure($this, $e, 0);
} catch (Throwable $e) {
// 其他异常
$result->addError($this, $e, 0);
}
// 注册测试结束
$result->endTest($this, 0);
return $result;
}
/**
* 执行测试方法
*/
protectedfunctionrunTest(): mixed{
// 获取测试方法名
$testMethod = $this->name;
// 检查方法是否存在
if (!method_exists($this, $testMethod)) {
thrownewException("Method {$testMethod} does not exist");
}
// 执行setUp
$this->setUp();
try {
// 调用测试方法
$testResult = $this->$testMethod(...$this->data);
// 验证Mock对象
$this->verifyMockObjects();
} finally {
// 执行tearDown
$this->tearDown();
}
return $testResult;
}
/**
* 测试前置方法
*/
protectedfunctionsetUp(): void{
// 子类重写
}
/**
* 测试后置方法
*/
protectedfunctiontearDown(): void{
// 子类重写
}
/**
* 类级别前置方法
*/
publicstaticfunctionsetUpBeforeClass(): void{
// 子类重写
}
/**
* 类级别后置方法
*/
publicstaticfunctiontearDownAfterClass(): void{
// 子类重写
}
}
2.2 测试生命周期
<?php
/**
* 测试生命周期详解
*/
classUserServiceTestextendsTestCase{
privatestatic PDO $pdo;
private UserService $userService;
/**
* 1、类级别初始化(只执行一次)
*/
publicstaticfunctionsetUpBeforeClass(): void{
echo"1. setUpBeforeClass - 创建数据库连接\n";
self::$pdo = new PDO('sqlite::memory:');
self::$pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
}
/**
* 2、每个测试前执行
*/
protectedfunctionsetUp(): void{
echo"2. setUp - 初始化测试对象\n";
$this->userService = new UserService(self::$pdo);
}
/**
* 3、测试方法
*/
publicfunctiontestCreateUser(): void{
echo"3. testCreateUser - 执行测试\n";
$user = $this->userService->create('John');
$this->assertNotNull($user->id);
}
publicfunctiontestFindUser(): void{
echo"3. testFindUser - 执行测试\n";
$user = $this->userService->find(1);
$this->assertEquals('John', $user->name);
}
/**
* 4、每个测试后执行
*/
protectedfunctiontearDown(): void{
echo"4. tearDown - 清理测试数据\n";
self::$pdo->exec('DELETE FROM users');
}
/**
* 5、类级别清理(只执行一次)
*/
publicstaticfunctiontearDownAfterClass(): void{
echo"5. tearDownAfterClass - 关闭数据库连接\n";
self::$pdo = null;
}
}
/*
执行顺序:
1. setUpBeforeClass - 创建数据库连接
2. setUp - 初始化测试对象
3. testCreateUser - 执行测试
4. tearDown - 清理测试数据
2. setUp - 初始化测试对象
3. testFindUser - 执行测试
4. tearDown - 清理测试数据
5. tearDownAfterClass - 关闭数据库连接
*/
三、断言系统源码分析
3.1 Assert基类
<?php
/**
* Assert断言基类源码分析
* 文件位置:phpunit/phpunit/src/Framework/Assert.php
*/
namespacePHPUnit\Framework;
abstractclassAssert{
/**
* 断言计数器
*/
privatestatic int $count = 0;
/**
* 断言相等
*/
publicstaticfunctionassertEquals(
mixed $expected,
mixed $actual,
string $message = ''
): void{
// 创建相等约束
$constraint = new IsEqual($expected);
// 执行断言
static::assertThat($actual, $constraint, $message);
}
/**
* 断言严格相等(===)
*/
publicstaticfunctionassertSame(
mixed $expected,
mixed $actual,
string $message = ''
): void{
$constraint = new IsIdentical($expected);
static::assertThat($actual, $constraint, $message);
}
/**
* 断言为真
*/
publicstaticfunctionassertTrue(mixed $condition, string $message = ''): void{
$constraint = new IsTrue();
static::assertThat($condition, $constraint, $message);
}
/**
* 断言为假
*/
publicstaticfunctionassertFalse(mixed $condition, string $message = ''): void{
$constraint = new IsFalse();
static::assertThat($condition, $constraint, $message);
}
/**
* 断言为null
*/
publicstaticfunctionassertNull(mixed $actual, string $message = ''): void{
$constraint = new IsNull();
static::assertThat($actual, $constraint, $message);
}
/**
* 断言不为null
*/
publicstaticfunctionassertNotNull(mixed $actual, string $message = ''): void{
$constraint = new LogicalNot(new IsNull());
static::assertThat($actual, $constraint, $message);
}
/**
* 断言数组包含
*/
publicstaticfunctionassertContains(
mixed $needle,
iterable $haystack,
string $message = ''
): void{
$constraint = new TraversableContains($needle);
static::assertThat($haystack, $constraint, $message);
}
/**
* 断言数组有键
*/
publicstaticfunctionassertArrayHasKey(
int|string $key,
array $array,
string $message = ''
): void{
$constraint = new ArrayHasKey($key);
static::assertThat($array, $constraint, $message);
}
/**
* 断言实例类型
*/
publicstaticfunctionassertInstanceOf(
string $expected,
mixed $actual,
string $message = ''
): void{
$constraint = new IsInstanceOf($expected);
static::assertThat($actual, $constraint, $message);
}
/**
* 核心断言方法
* 所有断言最终都调用此方法
*/
publicstaticfunctionassertThat(
mixed $value,
Constraint $constraint,
string $message = ''
): void{
// 增加断言计数
self::$count++;
// 评估约束
if (!$constraint->evaluate($value, '', true)) {
// 约束不满足,抛出失败异常
$failureDescription = $constraint->failureDescription($value);
thrownew AssertionFailedError(
$message ? $message . "\n" . $failureDescription : $failureDescription
);
}
}
/**
* 获取断言计数
*/
publicstaticfunctiongetCount(): int{
returnself::$count;
}
/**
* 重置断言计数
*/
publicstaticfunctionresetCount(): void{
self::$count = 0;
}
}
3.2 Constraint约束系统
<?php
/**
* Constraint约束基类
* 文件位置:phpunit/phpunit/src/Framework/Constraint/Constraint.php
*/
namespacePHPUnit\Framework\Constraint;
abstractclassConstraint{
/**
* 评估约束
*
* @param mixed $other 要评估的值
* @param string $description 描述
* @param bool $returnResult 是否返回结果而非抛出异常
*/
publicfunctionevaluate(
mixed $other,
string $description = '',
bool $returnResult = false
): ?bool{
$success = $this->matches($other);
if ($returnResult) {
return $success;
}
if (!$success) {
$this->fail($other, $description);
}
returnnull;
}
/**
* 检查值是否匹配约束
* 子类必须实现
*/
abstractprotectedfunctionmatches(mixed $other): bool;
/**
* 约束失败时抛出异常
*/
protectedfunctionfail(mixed $other, string $description): never{
thrownew ExpectationFailedException(
$this->failureDescription($other)
);
}
/**
* 生成失败描述
*/
protectedfunctionfailureDescription(mixed $other): string{
return$this->exporter()->export($other) . ' ' . $this->toString();
}
/**
* 约束的字符串表示
*/
abstractpublicfunctiontoString(): string;
}
3.3 常用约束实现
<?php
/**
* IsEqual约束实现
*/
namespacePHPUnit\Framework\Constraint;
classIsEqualextendsConstraint{
private mixed $value;
private float $delta;
private bool $canonicalize;
private bool $ignoreCase;
publicfunction__construct(
mixed $value,
float $delta = 0.0,
bool $canonicalize = false,
bool $ignoreCase = false
){
$this->value = $value;
$this->delta = $delta;
$this->canonicalize = $canonicalize;
$this->ignoreCase = $ignoreCase;
}
protectedfunctionmatches(mixed $other): bool{
// 使用Comparator进行比较
$comparatorFactory = ComparatorFactory::getInstance();
try {
$comparator = $comparatorFactory->getComparatorFor(
$this->value,
$other
);
$comparator->assertEquals(
$this->value,
$other,
$this->delta,
$this->canonicalize,
$this->ignoreCase
);
} catch (ComparisonFailure $f) {
returnfalse;
}
returntrue;
}
publicfunctiontoString(): string{
if (is_string($this->value)) {
if (strpos($this->value, "\n") !== false) {
return'is equal to <text>';
}
return sprintf("is equal to '%s'", $this->value);
}
return sprintf(
'is equal to %s',
$this->exporter()->export($this->value)
);
}
}
/**
* IsInstanceOf约束实现
*/
classIsInstanceOfextendsConstraint{
private string $className;
publicfunction__construct(string $className){
$this->className = $className;
}
protectedfunctionmatches(mixed $other): bool{
return $other instanceof$this->className;
}
publicfunctiontoString(): string{
return sprintf('is instance of %s', $this->className);
}
protectedfunctionfailureDescription(mixed $other): string{
return sprintf(
'%s is an instance of %s',
$this->exporter()->shortenedExport($other),
$this->className
);
}
}
/**
* LogicalNot约束实现(装饰器模式)
*/
classLogicalNotextendsConstraint{
private Constraint $constraint;
publicfunction__construct(Constraint $constraint){
$this->constraint = $constraint;
}
protectedfunctionmatches(mixed $other): bool{
return !$this->constraint->evaluate($other, '', true);
}
publicfunctiontoString(): string{
return'not ' . $this->constraint->toString();
}
}
四、Mock对象源码分析
4.1 MockBuilder构建器
<?php
/**
* MockBuilder源码分析
* 文件位置:phpunit/phpunit/src/Framework/MockObject/MockBuilder.php
*/
namespacePHPUnit\Framework\MockObject;
classMockBuilder{
/**
* 测试用例
*/
private TestCase $testCase;
/**
* 要Mock的类型
*/
private string $type;
/**
* 要Mock的方法
*/
privatearray $methods = [];
/**
* 构造函数参数
*/
privatearray $constructorArgs = [];
/**
* 是否禁用构造函数
*/
private bool $disableOriginalConstructor = false;
/**
* 是否禁用克隆
*/
private bool $disableOriginalClone = false;
publicfunction__construct(TestCase $testCase, string $type){
$this->testCase = $testCase;
$this->type = $type;
}
/**
* 设置要Mock的方法
*/
publicfunctiononlyMethods(array $methods): self{
$this->methods = $methods;
return$this;
}
/**
* 添加额外方法
*/
publicfunctionaddMethods(array $methods): self{
$this->methods = array_merge($this->methods, $methods);
return$this;
}
/**
* 设置构造函数参数
*/
publicfunctionsetConstructorArgs(array $args): self{
$this->constructorArgs = $args;
return$this;
}
/**
* 禁用原始构造函数
*/
publicfunctiondisableOriginalConstructor(): self{
$this->disableOriginalConstructor = true;
return$this;
}
/**
* 禁用原始克隆
*/
publicfunctiondisableOriginalClone(): self{
$this->disableOriginalClone = true;
return$this;
}
/**
* 构建Mock对象
*/
publicfunctiongetMock(): MockObject{
// 生成Mock类
$mockClass = $this->generate();
// 实例化Mock对象
if ($this->disableOriginalConstructor) {
$mock = $mockClass->newInstanceWithoutConstructor();
} else {
$mock = $mockClass->newInstanceArgs($this->constructorArgs);
}
// 注册到测试用例
$this->testCase->registerMockObject($mock);
return $mock;
}
/**
* 生成Mock类
*/
privatefunctiongenerate(): ReflectionClass{
$generator = new Generator();
return $generator->generate(
$this->type,
$this->methods,
'',
true,
true,
true,
$this->disableOriginalClone
);
}
}
4.2 Mock对象使用示例
<?php
/**
* Mock对象使用示例
*/
classOrderServiceTestextendsTestCase{
/**
* 测试创建订单
*/
publicfunctiontestCreateOrder(): void{
// 1、创建PaymentGateway的Mock
$paymentGateway = $this->createMock(PaymentGateway::class);
// 2、设置期望行为
$paymentGateway
->expects($this->once()) // 期望调用一次
->method('charge') // 方法名
->with( // 参数匹配
$this->equalTo(100.00),
$this->stringContains('order_')
)
->willReturn(true); // 返回值
// 3、注入Mock对象
$orderService = new OrderService($paymentGateway);
// 4、执行测试
$order = $orderService->create([
'amount' => 100.00,
'items' => ['item1', 'item2'],
]);
// 5、断言结果
$this->assertEquals('paid', $order->status);
}
/**
* 测试支付失败
*/
publicfunctiontestCreateOrderPaymentFailed(): void{
$paymentGateway = $this->createMock(PaymentGateway::class);
// 设置抛出异常
$paymentGateway
->method('charge')
->willThrowException(new PaymentException('Card declined'));
$orderService = new OrderService($paymentGateway);
// 期望抛出异常
$this->expectException(OrderException::class);
$this->expectExceptionMessage('Payment failed');
$orderService->create(['amount' => 100.00]);
}
/**
* 使用回调返回值
*/
publicfunctiontestWithCallback(): void{
$repository = $this->createMock(UserRepository::class);
$repository
->method('find')
->willReturnCallback(function($id){
returnnew User(['id' => $id, 'name' => "User {$id}"]);
});
$user = $repository->find(123);
$this->assertEquals('User 123', $user->name);
}
/**
* 连续返回不同值
*/
publicfunctiontestConsecutiveReturns(): void{
$queue = $this->createMock(Queue::class);
$queue
->method('pop')
->willReturnOnConsecutiveCalls('first', 'second', 'third');
$this->assertEquals('first', $queue->pop());
$this->assertEquals('second', $queue->pop());
$this->assertEquals('third', $queue->pop());
}
}
五、TestRunner运行器源码分析
5.1 TestRunner核心流程
<?php
/**
* TestRunner源码分析
* 文件位置:phpunit/phpunit/src/TextUI/TestRunner.php
*/
namespacePHPUnit\TextUI;
classTestRunner{
/**
* 运行测试
*/
publicfunctionrun(TestSuite $suite, array $arguments = []): TestResult{
// 1、创建测试结果收集器
$result = new TestResult();
// 2、添加监听器
$this->addListeners($result, $arguments);
// 3、设置代码覆盖率
if (isset($arguments['coverage'])) {
$result->setCodeCoverage($arguments['coverage']);
}
// 4、执行测试套件
$suite->run($result);
// 5、输出结果
$this->printResult($result);
return $result;
}
/**
* 添加监听器
*/
privatefunctionaddListeners(TestResult $result, array $arguments): void{
// 添加默认结果打印器
$result->addListener(new ResultPrinter(
$arguments['verbose'] ?? false
));
// 添加日志记录器
if (isset($arguments['log'])) {
$result->addListener(new LogListener($arguments['log']));
}
// 添加JUnit XML输出
if (isset($arguments['junitXml'])) {
$result->addListener(new JUnitXmlListener($arguments['junitXml']));
}
}
}
5.2 TestSuite测试套件
<?php
/**
* TestSuite源码分析
* 文件位置:phpunit/phpunit/src/Framework/TestSuite.php
*/
namespacePHPUnit\Framework;
classTestSuiteimplementsIteratorAggregate, Reorderable, SelfDescribing, Test{
/**
* 测试集合
*/
privatearray $tests = [];
/**
* 套件名称
*/
private string $name;
/**
* 测试数量
*/
private int $numTests = -1;
/**
* 从类创建测试套件
*/
publicstaticfunctionfromClassName(string $className): self{
$class = new ReflectionClass($className);
$suite = newself($className);
// 获取所有测试方法
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
if (self::isTestMethod($method)) {
$suite->addTest(
self::createTest($class, $method->getName())
);
}
}
return $suite;
}
/**
* 判断是否为测试方法
*/
privatestaticfunctionisTestMethod(ReflectionMethod $method): bool{
// 方法名以test开头
if (str_starts_with($method->getName(), 'test')) {
returntrue;
}
// 或者有@test注解
$docComment = $method->getDocComment();
if ($docComment && str_contains($docComment, '@test')) {
returntrue;
}
// 或者有#[Test]属性
$attributes = $method->getAttributes(Test::class);
if (!empty($attributes)) {
returntrue;
}
returnfalse;
}
/**
* 添加测试
*/
publicfunctionaddTest(Test $test): void{
$this->tests[] = $test;
$this->numTests = -1; // 重置计数
}
/**
* 运行测试套件
*/
publicfunctionrun(TestResult $result): void{
// 执行setUpBeforeClass
$this->invokeSetUpBeforeClass();
try {
// 遍历执行所有测试
foreach ($this->tests as $test) {
if ($result->shouldStop()) {
break;
}
$this->runTest($test, $result);
}
} finally {
// 执行tearDownAfterClass
$this->invokeTearDownAfterClass();
}
}
/**
* 运行单个测试
*/
protectedfunctionrunTest(Test $test, TestResult $result): void{
$test->run($result);
}
/**
* 获取测试数量
*/
publicfunctioncount(): int{
if ($this->numTests === -1) {
$this->numTests = 0;
foreach ($this->tests as $test) {
$this->numTests += count($test);
}
}
return$this->numTests;
}
}
5.3 TestResult结果收集器
<?php
/**
* TestResult源码分析
* 文件位置:phpunit/phpunit/src/Framework/TestResult.php
*/
namespacePHPUnit\Framework;
classTestResult{
/**
* 监听器列表
*/
privatearray $listeners = [];
/**
* 错误列表
*/
privatearray $errors = [];
/**
* 失败列表
*/
privatearray $failures = [];
/**
* 警告列表
*/
privatearray $warnings = [];
/**
* 跳过列表
*/
privatearray $skipped = [];
/**
* 运行的测试数
*/
private int $runTests = 0;
/**
* 是否停止
*/
private bool $stop = false;
/**
* 添加监听器
*/
publicfunctionaddListener(TestListener $listener): void{
$this->listeners[] = $listener;
}
/**
* 测试开始
*/
publicfunctionstartTest(Test $test): void{
$this->runTests++;
foreach ($this->listeners as $listener) {
$listener->startTest($test);
}
}
/**
* 测试结束
*/
publicfunctionendTest(Test $test, float $time): void{
foreach ($this->listeners as $listener) {
$listener->endTest($test, $time);
}
}
/**
* 添加错误
*/
publicfunctionaddError(Test $test, Throwable $t, float $time): void{
$this->errors[] = new TestFailure($test, $t);
foreach ($this->listeners as $listener) {
$listener->addError($test, $t, $time);
}
}
/**
* 添加失败
*/
publicfunctionaddFailure(
Test $test,
AssertionFailedError $e,
float $time
): void{
$this->failures[] = new TestFailure($test, $e);
foreach ($this->listeners as $listener) {
$listener->addFailure($test, $e, $time);
}
}
/**
* 添加跳过
*/
publicfunctionaddSkipped(Test $test, Throwable $t, float $time): void{
$this->skipped[] = new TestFailure($test, $t);
foreach ($this->listeners as $listener) {
$listener->addSkippedTest($test, $t, $time);
}
}
/**
* 是否成功
*/
publicfunctionwasSuccessful(): bool{
returnempty($this->errors) && empty($this->failures);
}
/**
* 获取统计信息
*/
publicfunctiongetStats(): array{
return [
'tests' => $this->runTests,
'assertions' => Assert::getCount(),
'errors' => count($this->errors),
'failures' => count($this->failures),
'warnings' => count($this->warnings),
'skipped' => count($this->skipped),
];
}
/**
* 停止测试
*/
publicfunctionstop(): void{
$this->stop = true;
}
/**
* 是否应该停止
*/
publicfunctionshouldStop(): bool{
return$this->stop;
}
}
六、数据提供器源码分析
6.1 DataProvider实现
<?php
/**
* DataProvider数据提供器
*/
classCalculatorTestextendsTestCase{
/**
* 数据提供器方法
*/
publicstaticfunctionadditionProvider(): array{
return [
'positive numbers' => [1, 2, 3],
'negative numbers' => [-1, -2, -3],
'zero' => [0, 0, 0],
'mixed' => [-1, 1, 0],
];
}
/**
* 使用数据提供器的测试
*
* @dataProvider additionProvider
*/
publicfunctiontestAdd(int $a, int $b, int $expected): void{
$calculator = new Calculator();
$this->assertEquals($expected, $calculator->add($a, $b));
}
/**
* 使用生成器的数据提供器
*/
publicstaticfunctionlargeDataProvider(): Generator{
for ($i = 0; $i < 1000; $i++) {
yield"case_{$i}" => [$i, $i * 2, $i * 3];
}
}
/**
* @dataProvider largeDataProvider
*/
publicfunctiontestWithLargeData(int $a, int $b, int $c): void{
$this->assertEquals($a + $b + $c, $a * 6);
}
}
6.2 DataProvider解析源码
<?php
/**
* DataProvider解析器
*/
namespacePHPUnit\Framework;
classDataProviderTestSuiteextendsTestSuite{
/**
* 从数据提供器创建测试
*/
publicstaticfunctionfromDataProvider(
ReflectionClass $class,
string $methodName,
string $dataProviderMethod
): self{
$suite = newself($class->getName() . '::' . $methodName);
// 获取数据提供器
$dataProvider = self::getDataFromProvider($class, $dataProviderMethod);
// 为每组数据创建测试
foreach ($dataProvider as $dataName => $data) {
$test = new ($class->getName())($methodName);
$test->setData($dataName, $data);
$suite->addTest($test);
}
return $suite;
}
/**
* 获取数据提供器数据
*/
privatestaticfunctiongetDataFromProvider(
ReflectionClass $class,
string $methodName
): iterable{
$method = $class->getMethod($methodName);
// 检查方法是否为静态
if (!$method->isStatic()) {
thrownewException(
"Data provider {$methodName} must be static"
);
}
// 调用数据提供器
$data = $method->invoke(null);
// 支持数组和生成器
if (is_array($data)) {
return $data;
}
if ($data instanceof Generator) {
return iterator_to_array($data);
}
thrownewException(
"Data provider must return array or Generator"
);
}
}
七、代码覆盖率源码分析
7.1 CodeCoverage实现
<?php
/**
* CodeCoverage代码覆盖率
* 文件位置:phpunit/php-code-coverage/src/CodeCoverage.php
*/
namespaceSebastianBergmann\CodeCoverage;
classCodeCoverage{
/**
* 驱动(Xdebug/PCOV)
*/
private Driver $driver;
/**
* 过滤器
*/
private Filter $filter;
/**
* 覆盖率数据
*/
privatearray $data = [];
/**
* 当前测试
*/
private ?string $currentId = null;
publicfunction__construct(Driver $driver, Filter $filter){
$this->driver = $driver;
$this->filter = $filter;
}
/**
* 开始收集覆盖率
*/
publicfunctionstart(string $id, bool $clear = false): void{
if ($clear) {
$this->clear();
}
$this->currentId = $id;
// 启动驱动收集
$this->driver->start();
}
/**
* 停止收集覆盖率
*/
publicfunctionstop(): array{
// 停止驱动收集
$data = $this->driver->stop();
// 过滤数据
$data = $this->filter->filter($data);
// 存储数据
$this->data[$this->currentId] = $data;
$this->currentId = null;
return $data;
}
/**
* 合并覆盖率数据
*/
publicfunctionmerge(CodeCoverage $that): void{
foreach ($that->data as $id => $data) {
if (isset($this->data[$id])) {
$this->data[$id] = $this->mergeData(
$this->data[$id],
$data
);
} else {
$this->data[$id] = $data;
}
}
}
/**
* 获取覆盖率报告
*/
publicfunctiongetReport(): Report{
$report = new Report();
foreach ($this->data as $id => $data) {
foreach ($data as $file => $lines) {
$report->addFile($file, $lines);
}
}
return $report;
}
/**
* 计算覆盖率百分比
*/
publicfunctiongetCoveragePercent(): float{
$report = $this->getReport();
$totalLines = $report->getTotalLines();
$coveredLines = $report->getCoveredLines();
if ($totalLines === 0) {
return100.0;
}
return ($coveredLines / $totalLines) * 100;
}
}
7.2 覆盖率报告生成
<?php
/**
* 覆盖率报告生成器
*/
// HTML报告
$coverage = new CodeCoverage($driver, $filter);
// 运行测试时收集覆盖率
$coverage->start('testMethod');
// ... 执行测试
$coverage->stop();
// 生成HTML报告
$writer = new Html\Facade();
$writer->process($coverage, '/path/to/report');
// 生成Clover XML报告
$writer = new Clover();
$writer->process($coverage, '/path/to/clover.xml');
// 生成文本报告
$writer = new Text();
echo $writer->process($coverage);
八、实战应用与最佳实践
8.1 完整测试示例
<?php
/**
* 完整的单元测试示例
*/
namespaceTests\Unit;
usePHPUnit\Framework\TestCase;
usePHPUnit\Framework\Attributes\Test;
usePHPUnit\Framework\Attributes\DataProvider;
usePHPUnit\Framework\Attributes\Depends;
useApp\Services\UserService;
useApp\Repositories\UserRepository;
useApp\Models\User;
classUserServiceTestextendsTestCase{
private UserService $userService;
private UserRepository $userRepository;
protectedfunctionsetUp(): void{
parent::setUp();
// 创建Mock
$this->userRepository = $this->createMock(UserRepository::class);
$this->userService = new UserService($this->userRepository);
}
/**
* 测试创建用户
*/
#[Test]
publicfunctioncreateUserSuccessfully(): User{
// Arrange
$userData = [
'name' => 'John Doe',
'email' => 'john@example.com',
];
$expectedUser = new User($userData);
$expectedUser->id = 1;
$this->userRepository
->expects($this->once())
->method('create')
->with($userData)
->willReturn($expectedUser);
// Act
$user = $this->userService->create($userData);
// Assert
$this->assertInstanceOf(User::class, $user);
$this->assertEquals('John Doe', $user->name);
$this->assertEquals('john@example.com', $user->email);
return $user;
}
/**
* 测试依赖:更新用户依赖创建用户
*/
#[Test]
#[Depends('createUserSuccessfully')]
publicfunctionupdateUserSuccessfully(User $user): void{
$updateData = ['name' => 'Jane Doe'];
$this->userRepository
->expects($this->once())
->method('update')
->with($user->id, $updateData)
->willReturn(true);
$result = $this->userService->update($user->id, $updateData);
$this->assertTrue($result);
}
/**
* 数据提供器测试
*/
publicstaticfunctioninvalidEmailProvider(): array{
return [
'empty email' => [''],
'no @ symbol' => ['invalid-email'],
'no domain' => ['test@'],
'no local part' => ['@example.com'],
'spaces' => ['test @example.com'],
];
}
#[Test]
#[DataProvider('invalidEmailProvider')]
publicfunctioncreateUserWithInvalidEmailFails(string $email): void{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid email');
$this->userService->create([
'name' => 'Test',
'email' => $email,
]);
}
/**
* 测试异常
*/
#[Test]
publicfunctionfindUserNotFound(): void{
$this->userRepository
->method('find')
->willReturn(null);
$this->expectException(\App\Exceptions\UserNotFoundException::class);
$this->userService->findOrFail(999);
}
/**
* 测试私有方法(通过反射)
*/
#[Test]
publicfunctiontestPrivateMethod(): void{
$reflection = new \ReflectionClass($this->userService);
$method = $reflection->getMethod('validateEmail');
$method->setAccessible(true);
$result = $method->invoke($this->userService, 'valid@email.com');
$this->assertTrue($result);
}
}
8.2 测试配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!-- phpunit.xml -->
<phpunitxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
stopOnFailure="false"
cacheResult="true"
cacheDirectory=".phpunit.cache">
<!-- 测试套件配置 -->
<testsuites>
<testsuitename="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuitename="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<!-- 代码覆盖率配置 -->
<coverage>
<include>
<directorysuffix=".php">src</directory>
</include>
<exclude>
<directory>src/Migrations</directory>
<file>src/helpers.php</file>
</exclude>
<report>
<htmloutputDirectory="coverage"/>
<cloveroutputFile="coverage/clover.xml"/>
<textoutputFile="coverage/coverage.txt"/>
</report>
</coverage>
<!-- PHP配置 -->
<php>
<envname="APP_ENV"value="testing"/>
<envname="DB_CONNECTION"value="sqlite"/>
<envname="DB_DATABASE"value=":memory:"/>
</php>
</phpunit>
九、总结
9.1 PHPUnit设计亮点
【设计模式应用】
1、模板方法模式
- TestCase定义测试骨架
- setUp/tearDown钩子
- 子类实现具体测试
2、组合模式
- TestSuite包含Test
- 递归执行测试
- 统一接口
3、观察者模式
- TestResult收集结果
- TestListener监听事件
- 解耦输出逻辑
4、建造者模式
- MockBuilder构建Mock
- 链式调用配置
- 灵活创建对象
5、策略模式
- Constraint约束系统
- 可替换的比较策略
- 扩展性强
9.2 核心要点
【学习要点】
1、TestCase核心
- 测试生命周期管理
- setUp/tearDown机制
- 断言方法封装
2、断言系统
- Constraint约束模式
- 可组合的断言
- 清晰的失败信息
3、Mock对象
- 测试替身创建
- 行为期望设置
- 依赖隔离
4、测试运行
- TestSuite组织测试
- TestResult收集结果
- 监听器扩展输出
9.3 最佳实践
【测试建议】
1、测试命名
- 清晰描述测试目的
- 使用test前缀或#[Test]属性
- 遵循given_when_then模式
2、测试结构
- Arrange-Act-Assert模式
- 每个测试只测一件事
- 保持测试独立
3、Mock使用
- 只Mock外部依赖
- 避免过度Mock
- 验证关键交互
4、数据提供器
- 边界值测试
- 异常情况覆盖
- 使用有意义的数据名
5、代码覆盖率
- 关注有意义的覆盖
- 不要追求100%
- 重点覆盖核心逻辑
通过本文的源码分析,我们深入了解了PHPUnit的架构设计和实现原理。PHPUnit采用了多种优秀的设计模式,提供了强大而灵活的测试能力。掌握这些知识,不仅能更好地编写测试,还能学习到优秀的软件设计思想。
夜雨聆风