本文目录导读:

我来详细介绍PHP的测试驱动开发(TDD)实践方法。
TDD基础概念
TDD核心循环(Red-Green-Refactor)
RED:先写失败的测试
2. GREEN:写最少代码让测试通过
3. REFACTOR:重构代码,保持测试通过
测试工具选择
主要测试框架
# PHPUnit(最流行) composer require --dev phpunit/phpunit # Pest(优雅语法,基于PHPUnit) composer require pestphp/pest --dev
实际案例演示
场景:开发一个计算器类
第一步:编写测试(RED)
使用PHPUnit:
<?php
// tests/CalculatorTest.php
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
private Calculator $calculator;
protected function setUp(): void
{
$this->calculator = new Calculator();
}
public function testAddTwoNumbers(): void
{
$result = $this->calculator->add(2, 3);
$this->assertEquals(5, $result);
}
public function testSubtractTwoNumbers(): void
{
$result = $this->calculator->subtract(5, 3);
$this->assertEquals(2, $result);
}
public function testMultiplyTwoNumbers(): void
{
$result = $this->calculator->multiply(4, 3);
$this->assertEquals(12, $result);
}
public function testDivideTwoNumbers(): void
{
$result = $this->calculator->divide(10, 2);
$this->assertEquals(5, $result);
}
public function testDivideByZeroThrowsException(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->calculator->divide(10, 0);
}
}
使用Pest(更简洁):
<?php
// tests/CalculatorTest.php
use function Pest\Laravel\post;
test('add two numbers', function () {
$calculator = new Calculator();
$result = $calculator->add(2, 3);
expect($result)->toBe(5);
});
it('divides two numbers', function () {
$calculator = new Calculator();
expect($calculator->divide(10, 2))->toBe(5);
});
it('throws exception when dividing by zero', function () {
$calculator = new Calculator();
$calculator->divide(10, 0);
})->throws(InvalidArgumentException::class);
第二步:运行测试(此时应该失败)
# PHPUnit vendor/bin/phpunit --filter testAddTwoNumbers # Pest vendor/bin/pest --filter add
第三步:编写最少代码(GREEN)
<?php
// src/Calculator.php
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;
}
}
第四步:重构(REFACTOR)
<?php
// src/Calculator.php
class Calculator
{
public function add(float $a, float $b): float
{
return $a + $b;
}
// 其他方法...
private function validateDivision(float $divisor): void
{
if ($divisor === 0.0) {
throw new InvalidArgumentException('Cannot divide by zero');
}
}
}
高级测试场景
数据提供器(Data Providers)
#[DataProvider('additionProvider')]
public function testAdditionWithMultipleCases($a, $b, $expected): void
{
$calculator = new Calculator();
$this->assertEquals($expected, $calculator->add($a, $b));
}
public function additionProvider(): array
{
return [
[0, 0, 0],
[1, 2, 3],
[-1, 1, 0],
[3.5, 2.5, 6.0],
];
}
测试Mock对象
<?php
// 测试一个依赖外部服务的类
class UserServiceTest extends TestCase
{
public function testUserCreationCallsApi(): void
{
// 创建Mock
$apiClient = $this->createMock(ApiClient::class);
$apiClient->expects($this->once())
->method('createUser')
->with($this->isType('array'))
->willReturn(['id' => 1, 'status' => 'created']);
// 注入依赖
$userService = new UserService($apiClient);
$result = $userService->createUser([
'name' => 'John',
'email' => 'john@example.com'
]);
$this->assertArrayHasKey('id', $result);
}
}
数据库测试
使用事务回滚
<?php
class UserRepositoryTest extends TestCase
{
use RefreshDatabase; // Laravel的测试特性
public function testFindUserByEmail(): void
{
// 创建测试数据
User::create([
'name' => 'John Doe',
'email' => 'john@example.com'
]);
$repository = new UserRepository();
$user = $repository->findByEmail('john@example.com');
$this->assertNotNull($user);
$this->assertEquals('John Doe', $user->name);
}
public function testFindNonExistentUser(): void
{
$repository = new UserRepository();
$user = $repository->findByEmail('nonexistent@example.com');
$this->assertNull($user);
}
}
使用内存数据库
<?php // phpunit.xml 配置 // <env name="DB_CONNECTION" value="sqlite"/> // <env name="DB_DATABASE" value=":memory:"/>
TDD最佳实践
测试命名规范
// 方法命名 testUserCanLogin() testLoginFailsWithWrongPassword() testAdminCannotDeleteOtherAdmins()
测试隔离原则
<?php
class OrderTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
// 每个测试前清理数据
Order::truncate();
}
public function tearDown(): void
{
// 清理Mock
Mockery::close();
parent::tearDown();
}
}
覆盖率检查
# 生成覆盖率报告 vendor/bin/phpunit --coverage-html build/coverage # 检查覆盖率 vendor/bin/phpunit --coverage-text
使用phpunit.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true"
stopOnFailure="false">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>
常见陷阱和解决方案
过度测试
// 不好的实践 - 测试实现细节
public function testAddCallsAdditionOperator()
{
$result = $calculator->add(2, 3);
$this->assertEquals(5, $result);
// 不要测试内部实现
}
// 好的实践 - 测试行为
public function testAddReturnsCorrectSum()
{
$result = $calculator->add(2, 3);
$this->assertEquals(5, $result);
}
私有方法测试
class CalculatorTest extends TestCase
{
public function testPrivateMethodUsingReflection(): void
{
$calculator = new Calculator();
$reflection = new \ReflectionClass($calculator);
$method = $reflection->getMethod('validateNumber');
$method->setAccessible(true);
// 测试私有方法
$this->assertNull($method->invoke($calculator, 10));
// 测试异常
$this->expectException(InvalidArgumentException::class);
$method->invoke($calculator, 'invalid');
}
}
集成到持续集成
# GitHub Actions配置示例
name: PHP Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
coverage: xdebug
- name: Install Dependencies
run: composer install --no-prod
- name: Run Tests
run: vendor/bin/phpunit --coverage-text
实际项目结构示例
├── src/
│ ├── Models/
│ ├── Services/
│ └── Controllers/
├── tests/
│ ├── Unit/
│ │ ├── Services/
│ │ └── Models/
│ ├── Feature/
│ │ └── Api/
│ └── TestCase.php
├── phpunit.xml
└── composer.json
TDD工作流建议
- 从需求出发:明确功能和边界条件
- 先写测试:一个功能一个测试
- 小步前进:每次只写足够的代码让测试通过
- 持续重构:保持代码简洁
- 测试优先级:先写核心业务逻辑的测试
TDD的核心是通过测试驱动设计,而不是单纯追求覆盖率,好的测试应该验证行为,而非实现细节。