PHP 怎么测试驱动开发

wen PHP项目 2

本文目录导读:

PHP 怎么测试驱动开发

  1. TDD基础概念
  2. 测试工具选择
  3. 实际案例演示
  4. 高级测试场景
  5. 数据库测试
  6. TDD最佳实践
  7. 常见陷阱和解决方案
  8. 集成到持续集成
  9. 实际项目结构示例
  10. TDD工作流建议

我来详细介绍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工作流建议

  1. 从需求出发:明确功能和边界条件
  2. 先写测试:一个功能一个测试
  3. 小步前进:每次只写足够的代码让测试通过
  4. 持续重构:保持代码简洁
  5. 测试优先级:先写核心业务逻辑的测试

TDD的核心是通过测试驱动设计,而不是单纯追求覆盖率,好的测试应该验证行为,而非实现细节。

抱歉,评论功能暂时关闭!