本文目录导读:

我来详细介绍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的核心是通过测试驱动设计,而不是单纯追求覆盖率,好的测试应该验证行为,而非实现细节。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播