PHPUnit
技术栈
工具链
phptestingunit-testingtddxunitcode-coverage安装
1. 环境准备
- OS:Linux / macOS / Windows
- PHP:>= 8.1(PHPUnit 10),>= 8.2(PHPUnit 11)
- PHP 扩展:json, dom, tokenizer, xml, xmlwriter
- 代码覆盖率:Xdebug 3.x 或 PCOV
- Composer:最新稳定版
2. 安装命令
项目级安装(推荐)
cd my-project
composer require --dev phpunit/phpunit
全局安装
composer global require phpunit/phpunit
验证安装
./vendor/bin/phpunit --version
# PHPUnit 11.0.x by Sebastian Bergmann and contributors.
生成配置文件
./vendor/bin/phpunit --generate-configuration
# 回答交互式问题生成 phpunit.xml
最小 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</directory>
</testsuite>
</testsuites>
</phpunit>
3. 常见安装问题
版本冲突
# PHPUnit 10 需要 PHP >= 8.1
composer require --dev phpunit/phpunit:^10.0
# PHPUnit 9 兼容 PHP 7.3+
composer require --dev phpunit/phpunit:^9.0
Xdebug 冲突
# 运行测试时禁用 Xdebug 提升性能
php -d xdebug.mode=off ./vendor/bin/phpunit
PCOV 配置(推荐替代 Xdebug)
pecl install pcov
# php.ini: extension=pcov.so
国内镜像
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
示例
Hello World:PHPUnit 第一个测试
目标
学习 PHPUnit 基础:创建测试类、编写断言、运行测试、理解测试结果。
完整代码
1. 项目结构
math-app/
├── src/
│ └── Calculator.php
├── tests/
│ └── CalculatorTest.php
├── phpunit.xml
└── composer.json
2. src/Calculator.php
<?php
namespace App;
class Calculator
{
public function add(float $a, float $b): float
{
return $a + $b;
}
public function subtract(float $a, float $b): float
{
return $a - $b;
}
public function multiply(float $a, float $b): float
{
return $a * $b;
}
public function divide(float $a, float $b): float
{
if ($b === 0.0) {
throw new \InvalidArgumentException('Division by zero');
}
return $a / $b;
}
public function fibonacci(int $n): array
{
if ($n < 0) {
throw new \InvalidArgumentException('Negative input');
}
$sequence = [0, 1];
for ($i = 2; $i <= $n; $i++) {
$sequence[] = $sequence[$i - 1] + $sequence[$i - 2];
}
return array_slice($sequence, 0, $n + 1);
}
}
3. tests/CalculatorTest.php
<?php
namespace App\Tests;
use App\Calculator;
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
private Calculator $calculator;
protected function setUp(): void
{
$this->calculator = new Calculator();
}
public function testAdd(): void
{
$this->assertSame(4.0, $this->calculator->add(2.0, 2.0));
$this->assertSame(0.0, $this->calculator->add(-1.0, 1.0));
}
public function testSubtract(): void
{
$this->assertSame(3.0, $this->calculator->subtract(5.0, 2.0));
}
public function testMultiply(): void
{
$this->assertSame(6.0, $this->calculator->multiply(2.0, 3.0));
}
public function testDivide(): void
{
$this->assertSame(2.5, $this->calculator->divide(5.0, 2.0));
}
public function testDivideByZeroThrowsException(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Division by zero');
$this->calculator->divide(5.0, 0.0);
}
#[DataProvider('fibonacciProvider')]
public function testFibonacci(int $input, array $expected): void
{
$this->assertSame($expected, $this->calculator->fibonacci($input));
}
public static function fibonacciProvider(): array
{
return [
'zero' => [0, [0]],
'one' => [1, [0, 1]],
'five' => [5, [0, 1, 1, 2, 3, 5]],
'ten' => [10, [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]],
];
}
public function testFibonacciNegativeThrowsException(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->calculator->fibonacci(-1);
}
}
运行步骤
./vendor/bin/phpunit
# 或带详细输出
./vendor/bin/phpunit --testdox
预期输出
PHPUnit 11.0.x by Sebastian Bergmann and contributors.
....... 7 / 7 (100%)
Time: 00:00.012, Memory: 6.00 MB
OK (7 tests, 15 assertions)
TestDox 格式:
Calculator
✔ Add
✔ Subtract
✔ Multiply
✔ Divide
✔ Divide by zero throws exception
✔ Fibonacci with data set "zero"
✔ Fibonacci with data set "one"
✔ Fibonacci with data set "five"
✔ Fibonacci with data set "ten"
✔ Fibonacci negative throws exception
常用断言速查
| 断言 | 用途 |
|---|---|
assertSame($a, $b) |
严格等于(类型+值) |
assertEquals($a, $b) |
宽松等于 |
assertTrue($x) |
为 true |
assertFalse($x) |
为 false |
assertNull($x) |
为 null |
assertCount($n, $arr) |
数组元素数 |
assertContains($v, $arr) |
包含某值 |
expectException(Class::class) |
预期异常 |
教程
PHPUnit 入门教程:实战 TDD 开发
1. 背景
测试驱动开发(TDD)是编写可靠 PHP 应用的核心实践。本教程通过构建一个 User 验证器,展示完整的 Red-Green-Refactor 循环。
2. 前置概念
| 概念 | 说明 |
|---|---|
| Red | 先写失败的测试 |
| Green | 用最少代码让测试通过 |
| Refactor | 重构代码,保持测试通过 |
| Mock | 模拟依赖对象 |
| @dataProvider | 数据驱动测试 |
3. 分步操作
步骤一:定义需求
我们要构建 UserValidator,验证用户输入:
- Email 格式必须正确
- 密码至少 8 个字符,包含大写字母和数字
- 用户名 3-20 字符,仅字母数字下划线
步骤二:编写测试 tests/UserValidatorTest.php
<?php
namespace App\Tests;
use App\UserValidator;
use PHPUnit\Framework\TestCase;
class UserValidatorTest extends TestCase
{
private UserValidator $validator;
protected function setUp(): void
{
$this->validator = new UserValidator();
}
// ========== Email 验证 ==========
#[DataProvider('validEmailsProvider')]
public function testValidEmails(string $email): void
{
$this->assertEmpty($this->validator->validateEmail($email));
}
#[DataProvider('invalidEmailsProvider')]
public function testInvalidEmails(string $email, string $expectedError): void
{
$errors = $this->validator->validateEmail($email);
$this->assertContains($expectedError, $errors);
}
public static function validEmailsProvider(): array
{
return [
['user@example.com'],
['user.name@domain.co.uk'],
['user+tag@domain.org'],
];
}
public static function invalidEmailsProvider(): array
{
return [
'empty' => ['', 'Email is required'],
'no_at' => ['userexample.com', 'Email is invalid'],
'no_domain'=> ['user@', 'Email is invalid'],
'spaces' => ['user @example.com', 'Email is invalid'],
];
}
// ========== 密码验证 ==========
#[DataProvider('validPasswordsProvider')]
public function testValidPasswords(string $password): void
{
$this->assertEmpty($this->validator->validatePassword($password));
}
#[DataProvider('invalidPasswordsProvider')]
public function testInvalidPasswords(string $password, string $expectedError): void
{
$errors = $this->validator->validatePassword($password);
$this->assertContains($expectedError, $errors);
}
public static function validPasswordsProvider(): array
{
return [
['Password1'],
['Str0ngP@ss'],
['Abcd1234'],
];
}
public static function invalidPasswordsProvider(): array
{
return [
'short' => ['Ab1', 'Password must be at least 8 characters'],
'no_uppercase' => ['password1', 'Password must contain an uppercase letter'],
'no_digit' => ['Password', 'Password must contain a digit'],
'too_short' => ['Pass1', 'Password must be at least 8 characters'],
];
}
// ========== 用户名验证 ==========
public function testValidUsername(): void
{
$this->assertEmpty($this->validator->validateUsername('john_doe'));
}
public function testUsernameTooShort(): void
{
$errors = $this->validator->validateUsername('ab');
$this->assertContains('Username must be between 3 and 20 characters', $errors);
}
public function testUsernameInvalidChars(): void
{
$errors = $this->validator->validateUsername('john doe!');
$this->assertContains('Username must only contain letters, numbers, and underscores', $errors);
}
// ========== 集成测试 ==========
#[DataProvider('validUsersProvider')]
public function testValidateAllFieldsValid(array $user): void
{
$errors = $this->validator->validate($user);
$this->assertEmpty($errors, 'All fields valid: ' . json_encode($errors));
}
public static function validUsersProvider(): array
{
return [
'normal' => [[
'username' => 'john_doe',
'email' => 'john@example.com',
'password' => 'Password1',
]],
'minimal' => [[
'username' => 'abc',
'email' => 'a@b.co',
'password' => 'Abcdefg1',
]],
];
}
}
步骤三:实现代码 src/UserValidator.php
<?php
namespace App;
class UserValidator
{
public function validate(array $data): array
{
$errors = [];
if (isset($data['email'])) {
$errors = array_merge($errors, $this->validateEmail($data['email']));
}
if (isset($data['password'])) {
$errors = array_merge($errors, $this->validatePassword($data['password']));
}
if (isset($data['username'])) {
$errors = array_merge($errors, $this->validateUsername($data['username']));
}
return $errors;
}
public function validateEmail(string $email): array
{
$errors = [];
if (empty($email)) {
$errors[] = 'Email is required';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Email is invalid';
}
return $errors;
}
public function validatePassword(string $password): array
{
$errors = [];
if (strlen($password) < 8) {
$errors[] = 'Password must be at least 8 characters';
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = 'Password must contain an uppercase letter';
}
if (!preg_match('/[0-9]/', $password)) {
$errors[] = 'Password must contain a digit';
}
return $errors;
}
public function validateUsername(string $username): array
{
$errors = [];
if (strlen($username) < 3 || strlen($username) > 20) {
$errors[] = 'Username must be between 3 and 20 characters';
}
if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
$errors[] = 'Username must only contain letters, numbers, and underscores';
}
return $errors;
}
}
步骤四:运行测试
./vendor/bin/phpunit --testdox --colors=always
4. TDD 循环总结
| 阶段 | 动作 |
|---|---|
| Red | 先写测试,运行确认失败 |
| Green | 写最少代码让测试通过 |
| Refactor | 改进代码结构,测试仍绿色 |
5. 思考题
- 如何使用 Mock 测试依赖外部 API 的类?
setUp()和setUpBeforeClass()的区别?- 如何生成 HTML/Clover 覆盖率报告?
参考资料
- [1] Sebastian Bergmann. PHPUnit Official Documentation. 2024. https://docs.phpunit.de
- [2] Sebastian Bergmann & Contributors. PHPUnit GitHub Repository. 2024. https://github.com/sebastianbergmann/phpunit
- [3] PHPUnit Team. Modern Testing in PHP with PHPUnit. 2024. https://phpunit.de/getting-started.html