测试覆盖率白名单配置

解读

国内中大型 PHP 项目上线前普遍要求单测覆盖率 ≥ 80%,但框架入口、实体、配置等“样板代码”无需纳入统计。白名单(whitelist)就是告诉覆盖率驱动(Xdebug、PCOV、php-code-coverage)“哪些文件必须被统计”,其余文件一律忽略,从而避免“分母膨胀”导致覆盖率虚低。面试官问“怎么配”不只是考语法,而是看候选人是否能把“业务无关文件”精准剔除、保证 CI 卡点一次通过,同时不污染生产 opcache。

知识点

  1. php-code-coverage 组件(PHPUnit 9.5+ 内置)的 <coverage><include><directory> 节点
  2. Xdebug 3 模式与 PCOV 的 ENABLE 开关差异
  3. filter 的三种写法:directory、file、suffix,优先级与合并规则
  4. whitelist 与 blacklist(exclude)同时存在时的“先白后黑”策略
  5. CI 场景:phpunit.xml.dist 与 phpunit.xml 的继承关系,防止开发者在本地把白名单改松
  6. 路径基准:基于 phpunit.xml 所在目录的相对路径,Docker 内若 WORKDIR 不同需做软链或前缀修正
  7. 性能:白名单过长(>2k 文件)时 PCOV 比 Xdebug 快 30%,建议高并发流水线切换驱动
  8. 上报 SonarQube、Codecov 时,xml 报告里仍需包含白名单外的文件,但标记为 0%,需要把 clover 的 false 属性设为 true,否则质量门禁误判

答案

以 Laravel 8 项目为例,业务代码集中在 app/ 与 modules/,框架启动文件、路由缓存、数据库迁移、视图编译文件全部忽略,CI 强制要求行覆盖率 ≥ 80%。在项目根目录创建 phpunit.xml.dist:

<?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"
         failOnRisky="true"
         failOnWarning="true">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory>tests/Integration</directory>
        </testsuite>
    </testsuites>

    <coverage cacheDirectory=".phpunit.cache/code-coverage"
              processUncoveredFiles="true">
        <!-- 白名单:只统计业务代码 -->
        <include>
            <directory suffix=".php">app</directory>
            <directory suffix=".php">modules</directory>
        </include>
        <!-- 黑名单:再精确剔除 -->
        <exclude>
            <directory>app/Console/Commands/Stubs</directory>
            <file>app/helpers.php</file>
            <directory>modules/*/Resources/stubs</directory>
        </exclude>
    </coverage>

    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value=":memory:"/>
    </php>
</phpunit>

关键点

  1. include 节点即白名单,只有出现在这里的文件才会进入覆盖率分子与分母;exclude 在白名单范围内二次过滤。
  2. cacheDirectory 开启缓存,二次运行提速 40% 以上,CI 中把 .phpunit.cache 目录加入 actions/cache。
  3. processUncoveredFiles="true" 确保白名单内未被测试覆盖的文件也计入分母,防止“只测一个文件就 100%”的作弊。
  4. 若使用 PCOV,在 GitHub Actions 加一步:
    - name: Enable PCOV
      run: |
        sudo phpdismod xdebug
        sudo phpenmod pcov
        echo "pcov.enabled=1" | sudo tee -a /etc/php/${{ matrix.php }}/mods-available/pcov.ini
    
    可让 10 万行代码的覆盖率采集从 90s 降到 12s。
  5. 上线门禁脚本:
    vendor/bin/phpunit --coverage-clover=coverage.xml
    php check-coverage.php coverage.xml 80
    
    check-coverage.php 读取 coverage.xml 的 project->metrics['elements'] 与 coveredelements,不达标 exit(1) 即可阻断合并。

拓展思考

  1. 微服务场景:各子服务用 monorepo 管理,如何在根目录统一白名单又允许服务自定义?
    答:在 phpunit.xml 用 <testsuite name="ServiceA"> 区分,每个服务下放 phpunit.service-a.xml,通过 vendor/bin/phpunit -c phpunit.service-a.xml 单独采集,SonarQube 多模块扫描即可。
  2. 本地开发想临时把覆盖率门槛降到 60%,但禁止提交:
    答:把 phpunit.xml 加入 .gitignore,本地随意改;CI 强制使用 phpunit.xml.dist,防止“放水”合并。
  3. 行覆盖、分支覆盖、路径覆盖混用:
    白名单只能控制“哪些文件”,分支覆盖率需 PHPUnit >=10 并打开 --coverage-branches,此时要把 include 范围进一步缩到核心业务类,否则分支爆炸导致报告过大,GitLab 页面会 502。
  4. 生产环境 opcache 预加载:
    白名单文件越少,预加载脚本越容易维护;可把 app/ 与 modules/ 目录打包为 preload.php,忽略 tests/ 与 vendor/,既加速又避免把测试类带进 opcache。