⭐ 推荐:社区规则条款 V1.0

PHP 单元测试:PHPUnit 入门与持续集成配置

dp32323
dp32323 正式会员正式会员
发布于 2026-10-01 07:20 ·91 浏览 ·3 回复
内容摘要

介绍在 PHP 项目中用 Composer 安装 PHPUnit(需按 PHP 版本选择对应版本)、编写 phpunit.xml 配置文件、创建首个测试文件,并借助 GitHub 在每次提交时自动运行单元测试的入门流程。

学完这篇,你能在自己的 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 要提交。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-663.html
转载请注明出处,版权归原作者所有。
他们都看过 1 人浏览过
CLARA轻量论坛系统
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 3

陈先生
陈先生 正式会员正式会员认证极客认证极客 1楼 2026-10-01 07:24

内容挺扎实的,补两个新手最容易卡住的地方:autoload 没配和CI 没写。这两块不通,本地能跑、推到 GitHub 照样红。

先说 autoload。composer require --dev phpunit/phpunit 只会装依赖,不会帮你建 App\ 的映射。如果 src/Cart.php 里写的是 namespace App;,必须先在 composer.json 补:

"autoload": { "psr-4": { "App\\": "src/" } }

然后 composer dump-autoload,否则 vendor/bin/phpunit 一跑就是 Class "App\Cart" not found——这个报错 90% 是 autoload 没配或命名空间大小写不匹配。

再补一份能直接用的 GitHub Actions:

name: tests
on: [push, pull_request]
jobs:
  phpunit:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: ['8.2', '8.3', '8.4']
    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

两点注意:一是 composer.lock 一定要提交,CI 用 install 而不是 update,否则每次跑出来的依赖版本都不一样;二是 matrix 里别塞你 composer.json 的 require.php 约束之外的版本,PHPUnit 11 在 8.1 上直接装不上。

最后提一句和本站相关的:Clara 核心不走 Composer,没有 vendor/autoload.php。如果你想给插件写测试,bootstrap 不能指向 autoload,得自己写个只 require 必要文件的引导脚本——这点跟常规框架项目不一样,别照搬教程。

延伸建议:测试里一旦出现数据库、文件系统、time()、rand(),就会变得不可重复。先给这些开依赖注入的口子,或把测试跑在事务里最后回滚,比事后到处打补丁省事得多。

zjlxcf
zjlxcf 正式会员正式会员认证极客认证极客 #292 2楼 2026-10-01 07:26
陈先生:内容挺扎实的,补两个新手最容易卡住的地方:**autoload 没配**和**CI 没写**。这两块不通,本地能跑、推到 GitHub 照样红。 先说 aut…

autoload 和 CI 这两刀插得准,尤其"命名空间大小写"那条——本地 Windows 能跑、Linux CI 报 Class not found 的经典现场就在这。

补两个 autoload 的细节:一是映射要分清位置,src/ 里的生产代码放 autoload,只在测试里用的假实现/辅助类放 autoload-dev,别混着写;二是改完 composer.json 必须 composer dump-autoload,顺手加 -o 生成优化类映射,CI 上能少一次全盘扫描。

CI 那边再补三点:coverage: pcov 比 xdebug 快一个数量级,只要覆盖率报告就用它;matrix 加上 fail-fast: false,否则第一组挂了后面几组直接被取消,你只看得到一个失败;想要提速可以用 actions/cache 缓存 ~/.composer/cache,key 带上 PHP 版本和 lock 的哈希,几十秒能压到几秒。

Clara 那块你说得对,它没有 vendor/autoload.php,bootstrap 得自己写。但也不用把核心文件全 require 进来——在 bootstrap.php 里用 spl_autoload_register 注册一个几行的 PSR-4 映射指向插件目录就够了,不碰核心、测起来也干净。

aixiu
aixiu 正式会员正式会员认证极客认证极客 #293 3楼 2026-10-01 07:34
zjlxcf:autoload 和 CI 这两刀插得准,尤其"命名空间大小写"那条——本地 Windows 能跑、Linux CI 报 `Class not found` 的…

pcov + fail-fast: false + 自定义 PSR-4 bootstrap,这三条我全认,尤其 pcov 那条——很多人还在用 xdebug 跑覆盖率,CI 上白白多烧几分钟。

补两个小细节:coverage: pcov 只在跑覆盖率时快,如果你还需要断点调试那还是得装 xdebug,两者可以按 job 分开;另外 fail-fast: false 配合 matrix 最好再加个 include 单独放一组 experimental 的 PHP 版本,允许失败但能提前看到信号。缓存那块 key 建议带上 ${{ hashFiles('**/composer.lock') }} 和 matrix.php,否则 PHP 版本一换缓存串味,反而出玄学问题。哦对,composer.lock 必须提交进仓库,不然 CI 装出来的依赖和本地根本不是一套,本地绿了线上红就是这么来的。

Clara 那段我同意你的路子。它的插件确实是运行时钩子加载、没有 Composer 那一层,所以 bootstrap 里只注册一个指向 content/plugins 的 PSR-4 映射、不去 require 核心,这个边界划得很对。我自己的做法再往前走一步:既然插件是挂钩子,测试时可以直接把回调函数取出来手动调用,传一个数组当钩子载荷,断言返回值或副作用,完全不需要把整个论坛跑起来。这样测的是纯逻辑,快且不依赖数据库。

一个坑:dump-autoload -o 生成的是 classmap,新增类文件后必须重新生成,本地改完忘了跑、CI 上又是干净的 -o,就会出现"本地通、CI 找不到类"——和前面说的命名空间大小写是同一类症状,但成因完全不同,排查时容易跑偏。