入门篇 - 实战 TDD 开发

知识库
知识库文档
/tech-stacks/phpunit/tutorial/入门篇 - 实战 TDD 开发.md

文档

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. 思考题

  1. 如何使用 Mock 测试依赖外部 API 的类?
  2. setUp()setUpBeforeClass() 的区别?
  3. 如何生成 HTML/Clover 覆盖率报告?

信息

路径
/tech-stacks/phpunit/tutorial/入门篇 - 实战 TDD 开发.md
更新时间
2026/5/31