乐于分享
好东西不私藏

PHPUnit源码解析

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;

abstractclassTestCaseextendsAssertimplementsReorderableSelfDescribingTest{

/**
     * 测试名称
     */

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($this0);

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;

classTestSuiteimplementsIteratorAggregateReorderableSelfDescribingTest{

/**
     * 测试集合
     */

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' => [123],
'negative numbers' => [-1-2-3],
'zero' => [000],
'mixed' => [-110],
        ];
    }

/**
     * 使用数据提供器的测试
     * 
     * @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采用了多种优秀的设计模式,提供了强大而灵活的测试能力。掌握这些知识,不仅能更好地编写测试,还能学习到优秀的软件设计思想。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » PHPUnit源码解析

猜你喜欢

  • 暂无文章