文档
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 覆盖率报告?