强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 20 章 — 测试

第 20 章 — 测试:PHPUnit、Mockery 与代码覆盖率

20.1 PHPUnit 基础

composer require --dev phpunit/phpunit
<?php
// tests/Unit/CalculatorTest.php
declare(strict_types=1);

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use App\Calculator;

class CalculatorTest extends TestCase
{
    private Calculator $calc;

    protected function setUp(): void
    {
        $this->calc = new Calculator();
    }

    public function testAdd(): void
    {
        $this->assertEquals(4, $this->calc->add(2, 2));
        $this->assertEquals(0, $this->calc->add(-1, 1));
    }

    public function testDivideByZeroThrows(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->calc->divide(10, 0);
    }

    #[\PHPUnit\Framework\Attributes\DataProvider('additionProvider')]
    public function testAddWithDataProvider(int $a, int $b, int $expected): void
    {
        $this->assertEquals($expected, $this->calc->add($a, $b));
    }

    public static function additionProvider(): array
    {
        return [
            'positive'  => [1, 2, 3],
            'negative'  => [-1, -2, -3],
            'zero'      => [0, 0, 0],
            'mixed'     => [-1, 1, 0],
        ];
    }
}

20.2 常用断言

断言说明
assertEquals($expected, $actual)相等(松散)
assertSame($expected, $actual)全等(严格)
assertTrue($condition)为 true
assertFalse($condition)为 false
assertNull($value)为 null
assertNotNull($value)不为 null
assertCount($count, $array)数组元素数
assertEmpty($value)为空
assertContains($needle, $haystack)包含
assertStringContainsString($needle, $haystack)字符串包含
assertInstanceOf($class, $object)实例类型
assertArrayHasKey($key, $array)数组有键
assertJson($string)有效 JSON

20.3 数据提供者

<?php
class ValidationTest extends TestCase
{
    #[\PHPUnit\Framework\Attributes\DataProvider('emailProvider')]
    public function testEmailValidation(string $email, bool $expected): void
    {
        $this->assertSame($expected, filter_var($email, FILTER_VALIDATE_EMAIL) !== false);
    }

    public static function emailProvider(): array
    {
        return [
            'valid simple'     => ['[email protected]', true],
            'valid subdomain'  => ['[email protected]', true],
            'invalid no at'    => ['userexample.com', false],
            'invalid no domain' => ['user@', false],
            'empty'            => ['', false],
        ];
    }
}

20.4 Mock 与 Stub

20.4.1 PHPUnit 内置 Mock

<?php
class UserServiceTest extends TestCase
{
    public function testGetUserReturnsFormattedData(): void
    {
        // 创建 Mock
        $repository = $this->createMock(UserRepository::class);
        $repository->method('findById')
            ->with(1)
            ->willReturn(['id' => 1, 'name' => 'Alice']);

        $service = new UserService($repository);
        $user = $service->getUser(1);

        $this->assertEquals('Alice', $user['name']);
    }

    public function testSaveCallsRepository(): void
    {
        $repository = $this->createMock(UserRepository::class);
        $repository->expects($this->once())
            ->method('save')
            ->with($this->callback(fn($user) => $user['name'] === 'Alice'))
            ->willReturn(true);

        $service = new UserService($repository);
        $result = $service->createUser(['name' => 'Alice']);

        $this->assertTrue($result);
    }
}

20.4.2 Mockery

composer require --dev mockery/mockery
<?php
use Mockery\Adapter\Phpunit\MockeryTestCase;

class OrderServiceTest extends MockeryTestCase
{
    public function testProcessOrder(): void
    {
        $payment = \Mockery::mock(PaymentGateway::class);
        $payment->shouldReceive('charge')
            ->once()
            ->with(99.99, 'USD')
            ->andReturn(['status' => 'success', 'transaction_id' => 'txn_123']);

        $mailer = \Mockery::mock(Mailer::class);
        $mailer->shouldReceive('send')
            ->once()
            ->with(\Mockery::on(fn($email) => $email->getTo() === '[email protected]'));

        $service = new OrderService($payment, $mailer);
        $result = $service->process(new Order(amount: 99.99, email: '[email protected]'));

        $this->assertTrue($result->isSuccessful());
    }
}

20.5 集成测试

<?php
declare(strict_types=1);

use PHPUnit\Framework\TestCase;
use PDO;

class UserRepositoryTest extends TestCase
{
    private PDO $db;

    protected function setUp(): void
    {
        $this->db = new PDO('sqlite::memory:');
        $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->db->exec('
            CREATE TABLE users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                email TEXT UNIQUE NOT NULL,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP
            )
        ');
    }

    public function testInsertAndRetrieve(): void
    {
        $repo = new UserRepository($this->db);

        $user = new User('Alice', '[email protected]');
        $this->assertTrue($repo->save($user));

        $found = $repo->findByEmail('[email protected]');
        $this->assertNotNull($found);
        $this->assertEquals('Alice', $found->getName());
    }

    public function testEmailUniqueness(): void
    {
        $repo = new UserRepository($this->db);

        $repo->save(new User('Alice', '[email protected]'));

        $this->expectException(\PDOException::class);
        $repo->save(new User('Bob', '[email protected]'));
    }
}

20.6 代码覆盖率

<!-- phpunit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory>tests/Feature</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory>src</directory>
        </include>
    </source>
</phpunit>
# 运行测试
vendor/bin/phpunit

# 生成覆盖率报告
vendor/bin/phpunit --coverage-html=coverage/
vendor/bin/phpunit --coverage-clover=coverage.xml

20.7 业务场景:API 测试

<?php
class UserApiTest extends TestCase
{
    public function testCreateUser(): void
    {
        $response = $this->json('POST', '/api/users', [
            'name'  => 'Alice',
            'email' => '[email protected]',
        ]);

        $response->assertStatus(201);
        $response->assertJsonStructure(['id', 'name', 'email']);
        $response->assertJson(['name' => 'Alice']);
    }

    public function testCreateUserWithInvalidEmail(): void
    {
        $response = $this->json('POST', '/api/users', [
            'name'  => 'Alice',
            'email' => 'not-an-email',
        ]);

        $response->assertStatus(422);
        $response->assertJsonValidationErrors(['email']);
    }
}

20.8 扩展阅读


上一章第 19 章 — HTTP 编程 下一章第 21 章 — 日志