学完这篇,你能在自己的 PHP 项目里跑起第一个单元测试,并让 GitHub 在每次提交时自动帮你跑一遍。
第一步:装 PHPUnit
PHPUnit 走 Composer 分发,在项目根目录执行:
composer require --dev phpunit/phpunit ^11
装完验证:
vendor/bin/phpunit --version
注意:PHPUnit 版本和 PHP 版本是强绑定的。PHPUnit 11 要 PHP 8.2+,PHPUnit 10 要 PHP 8.1+,PHP 7.4 的项目只能用 PHPUnit 9.x。装最新版在旧 PHP 上会直接依赖解析失败,别硬试。
第二步:写配置文件
根目录新建 `phpunit.xml`:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="unit">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
`tests` 是测试代码目录,`src` 是被测代码目录,按你的实际结构改。`<source>` 是 PHPUnit 10 之后的新写法,老教程里的 `<filter><whitelist>` 已经废弃。
第三步:写第一个测试
假设有个 `src/Cart.php`,测试文件放 `tests/CartTest.php`:
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use App\Cart;
final class CartTest extends TestCase
{
public function testEmptyTotalIsZero(): void
{
$this->assertSame(0, (new Cart())->total());
}
}
跑一下:
vendor/bin/phpunit
文件命名必须 `*Test.php`,类必须继承 `TestCase`,方法名以 `test` 开头(或加 `#[Test]` 属性)。
常用断言:`assertSame`(类型和值都相等,优先用这个)、`assertEquals`、`assertTrue`、`assertNull`、`assertCount`、`expectException`。
第四步:参数化与替身
同一段逻辑要测多组数据,用数据提供器,别复制粘贴:
#[DataProvider('priceProvider')]
public function testTotal(array $prices, int $expected): void
{
$cart = new Cart();
foreach ($prices as $p) {
$cart->add($p);
}
$this->assertSame($expected, $cart->total());
}
public static function priceProvider(): array
{
return [
'单件' => [[100], 100],
'多件' => [[100, 250], 350],
];
}
注意:PHPUnit 10 之后数据提供器必须是 `public static`,写成普通方法会直接报错——这是从老版本迁移时最常见的一处改动。
要隔离数据库、HTTP 这类外部依赖,用 `createMock()` 造替身,断言调用行为而不是真实副作用:
$mailer = $this->createMock(Mailer::class);
$mailer->expects($this->once())->method('send');
第五步:看覆盖率
vendor/bin/phpunit --coverage-text
注意:覆盖率依赖 Xdebug 或 PCOV 扩展,没装的话 PHPUnit 只会给一行警告,不给数字。别以为是自己配置写错了。
覆盖率是参考值不是 KPI,追 100% 通常换来一堆没意义的断言。核心业务逻辑(金额计算、权限判断、状态流转)优先覆盖。
第六步:接入持续集成
在项目根目录建 `.github/workflows/tests.yml`:
name: tests
on: [push, pull_request]
jobs:
phpunit:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.2', '8.3']
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
- run: composer install --prefer-dist --no-progress
- run: vendor/bin/phpunit --coverage-text
提交后到仓库的 Actions 页就能看到结果,失败会标红并把断言差异打印出来。
几个要点:
- 矩阵里列几个 PHP 版本,就相当于同时验证几个环境,避免"我本地能跑"。
- 测试要连 MySQL 的话,用 job 级 `services:` 起一个 mysql 容器,再把连接参数通过环境变量传进去。
- `composer.lock` 必须提交,CI 里才能装出与本地一致的依赖版本;`vendor/` 则要写进 `.gitignore`。
注意:CI 是无状态环境。任何依赖本地文件、本地数据库、绝对路径的测试都会挂,写测试时就要把这些东西做成可注入的配置。
用 GitLab 的话思路一样,`.gitlab-ci.yml` 里换成 `image: php:8.3`,加 `before_script: composer install` 即可。
小结
- 用 `composer require --dev phpunit/phpunit` 安装,PHPUnit 版本必须匹配 PHP 版本。
- `phpunit.xml` 里配 `testsuites` 和 `source`,后者是新版写法。
- 测试类继承 `TestCase`,断言优先用 `assertSame`。
- 数据提供器必须 `public static`,外部依赖用 `createMock()` 隔离。
- 覆盖率需要 Xdebug/PCOV;CI 用 GitHub Actions 矩阵跑多 PHP 版本,`composer.lock` 要提交。