怎样为PHP项目编写单元测试?

wen java案例 2

PHP单元测试实战指南:从入门到CI集成

目录导读

  1. 为什么需要单元测试
  2. 环境搭建与工具选择
  3. 编写第一个测试用例
  4. 进阶测试模式
  5. 常见问题问答
  6. 持续集成中的测试集成

为什么需要单元测试

许多PHP开发者认为测试只是锦上添花,但实际上,没有测试的代码库就像没有安全网的高空杂技,单元测试通过验证函数、类和方法的行为,确保每次代码变更都不会破坏已有功能,在电商、金融等场景中,一次未捕获的bug可能导致直接经济损失。

怎样为PHP项目编写单元测试?

核心收益

  • 快速定位回归错误
  • 降低重构风险
  • 形成可执行的文档
  • 提升代码质量

环境搭建与工具选择

推荐工具栈

  • PHPUnit(官方标准,支持数据提供者、mock对象)
  • Mockery(灵活mock框架,适合复杂依赖)
  • Codeception(兼容验收测试,适合全栈测试)

安装步骤(Composer)

composer require --dev phpunit/phpunit mockery/mockery

目录结构建议

project/
├── tests/
│   ├── Unit/
│   │   └── UserServiceTest.php
│   └── bootstrap.php
├── src/
│   └── Services/
│       └── UserService.php
├── phpunit.xml
└── composer.json

配置文件(phpunit.xml)

<phpunit bootstrap="tests/bootstrap.php" colors="true">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
    </testsuites>
</phpunit>

编写第一个测试用例

目标:测试UserService::createUser方法

被测试类

// src/Services/UserService.php
class UserService {
    public function __construct(private Mailer $mailer) {}
    public function createUser(array $data): User {
        if (empty($data['email'])) {
            throw new \InvalidArgumentException('Email required');
        }
        $user = new User($data);
        $this->mailer->sendWelcome($user);
        return $user;
    }
}

测试类

// tests/Unit/UserServiceTest.php
use PHPUnit\Framework\TestCase;
use Mockery as m;
class UserServiceTest extends TestCase {
    protected function tearDown(): void {
        m::close();
    }
    public function testCreateUserWithValidDataReturnsUser() {
        $mailerMock = m::mock(Mailer::class);
        $mailerMock->shouldReceive('sendWelcome')->once();
        $service = new UserService($mailerMock);
        $data = ['email' => 'test@example.com', 'name' => '张三'];
        $result = $service->createUser($data);
        $this->assertInstanceOf(User::class, $result);
        $this->assertEquals('test@example.com', $result->email);
    }
    public function testCreateUserWithoutEmailThrowsException() {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Email required');
        $mailerMock = m::mock(Mailer::class);
        $service = new UserService($mailerMock);
        $service->createUser([]);
    }
}

运行测试

./vendor/bin/phpunit

进阶测试模式

数据提供者(重复测试不同输入)

/** @dataProvider userProvider */
public function testCreateUserWithDifferentNames($name, $expected) {
    // ...
}
public function userProvider(): array {
    return [
        ['张三', 3],    // 中文名长度?
        ['John', 4],
        ['', 0],
    ];
}

测试依赖(顺序执行)

public function testEmptyArray() {
    $this->assertEmpty([]);
    return [];
}
/** @depends testEmptyArray */
public function testPush(array $array) {
    array_push($array, 'item');
    $this->assertCount(1, $array);
    return $array;
}

Mock外部HTTP请求

$httpClient = m::mock(GuzzleHttp\Client::class);
$httpClient->shouldReceive('get')
    ->with('https://api.example.com/users')
    ->andReturn(new Response(200, [], json_encode(['id' => 1])));

常见问题问答

Q1:单元测试应该覆盖哪些方法?
A:优先覆盖核心业务逻辑(如订单计算、权限验证)、边界条件(空值、最大长度)、异常路径,基础getter/setter可以不测,除非包含业务逻辑。

Q2:测试数据库调用怎么办?
A:避免真实数据库,使用内存SQLite或测试容器(如Testcontainers for PHP),方法包括:

  • 事务回滚(测试后自动回滚)
  • Mock数据库连接
  • 使用Repository模式并mock Repository

Q3:测试私有或保护方法吗?
A:通常不直接测试,两种变通方案:

  1. 通过公有方法间接测试(更推荐)
  2. 使用反射工具(不推荐,破坏封装)

Q4:测试覆盖率多少才算合格?
A:建议核心模块达到80%以上,但不追求100%(如配置加载、简单委托方法),使用phpunit --coverage-html生成可视化报告。

Q5:测试运行太慢怎么办?
A:- 将慢速测试(如集成测试)标记为@group slow并使用--exclude-group跳过

  • 使用并行测试工具(Paratest)
  • 优先优化频繁执行的单元测试

持续集成中的测试集成

GitHub Actions配置示例

name: PHP Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          tools: phpunit
      - name: Install dependencies
        run: composer install --prefer-dist --no-progress
      - name: Run tests
        run: phpunit --coverage-text --coverage-clover=coverage.xml

测试金字塔实践

  • 单元测试:数量最多,速度快,占总测试的70%
  • 集成测试:测试模块间协作,20%
  • E2E测试:关键业务流程,10%

通过构建完善的单元测试体系,你的PHP项目将获得更高的稳定性与可维护性,测试不是额外负担,而是对代码未来的投资,从今天开始,为你的核心类编写第一个通过/失败的测试吧!

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