PHP 完全指南 / 第 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' => ['user@example.com', true],
'valid subdomain' => ['user@mail.example.com', 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() === 'alice@example.com'));
$service = new OrderService($payment, $mailer);
$result = $service->process(new Order(amount: 99.99, email: 'alice@example.com'));
$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', 'alice@example.com');
$this->assertTrue($repo->save($user));
$found = $repo->findByEmail('alice@example.com');
$this->assertNotNull($found);
$this->assertEquals('Alice', $found->getName());
}
public function testEmailUniqueness(): void
{
$repo = new UserRepository($this->db);
$repo->save(new User('Alice', 'alice@example.com'));
$this->expectException(\PDOException::class);
$repo->save(new User('Bob', 'alice@example.com'));
}
}
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' => 'alice@example.com',
]);
$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 章 — 日志