PHPUnit + Symfony:src目录外模块代码覆盖率为0%的问题
Symfony多模块项目代码覆盖率异常排查
问题描述
Symfony项目中,核心代码位于src/(命名空间App\),modules/下包含多个独立模块(各有自身composer.json与命名空间),所有测试均可正常执行,但通过PhpStorm或PHPUnit的--coverage-html生成覆盖率时,modules/下的模块代码覆盖率始终为0%。仅将模块代码移入App命名空间时覆盖率正常,但不符合项目架构需求。
项目结构
Project | composer.json | src/ (namespace App\) | tests/ (namespace App\Tests\) | modules/ --| Airport/ --| src/ (namespace Module\Airport\) --| tests/ (namespace Module\Airport\Tests\) --| composer.json --| OtherModule/ --| src/ (namespace Module\OtherModule\) --| tests/ (namespace Module\OtherModule\Tests\) --| composer.json
PHPUnit配置
<?xml version="1.0" encoding="UTF-8"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" colors="true" bootstrap="tests/bootstrap.php" > <php> <ini name="error_reporting" value="-1"/> <server name="APP_ENV" value="test" force="true"/> <env name="APP_ENV" value="test" force="true"/> <server name="SHELL_VERBOSITY" value="-1"/> <server name="SYMFONY_PHPUNIT_REMOVE" value=""/> <server name="SYMFONY_PHPUNIT_VERSION" value="9.5"/> <server name="KERNEL_CLASS" value="App\Kernel"/> </php> <testsuites> <testsuite name="Project Test Suite"> <directory>tests</directory> <!-- Core tests --> <directory>modules/*/tests</directory> <!-- Multiple modules tests --> </testsuite> </testsuites> <coverage> <include> <directory suffix=".php">src</directory> <!-- Core coverage --> <directory suffix=".php">modules/*/src</directory> <!-- Multiple modules coverage --> </include> </coverage> <!--<listeners> <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/> </listeners>--> <extensions> <extension class="\DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/> </extensions> </phpunit>
环境版本
- PHPUnit 9.5.20
- PHP 7.4.28
- Xdebug 3.1.5
Xdebug配置
/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini, xdebug Support Xdebug on Patreon, GitHub, or as a business: https://xdebug.org/support Enabled Features (through 'xdebug.mode' setting) xdebug.auto_trace => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.cli_color => 0 => 0 xdebug.client_discovery_header => no value => no value xdebug.client_host => 172.17.0.1 => 172.17.0.1 xdebug.client_port => 9003 => 9003 xdebug.cloud_id => no value => no value xdebug.collect_assignments => Off => Off xdebug.collect_includes => (setting removed in Xdebug 3) => (setting removed in Xdebug 3) xdebug.collect_params => (setting removed in Xdebug 3) => (setting removed in Xdebug 3) xdebug.collect_return => Off => Off xdebug.collect_vars => (setting removed in Xdebug 3) => (setting removed in Xdebug 3) xdebug.connect_timeout_ms => 200 => 200 xdebug.coverage_enable => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.default_enable => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.discover_client_host => Off => Off xdebug.dump.COOKIE => no value => no value xdebug.dump.ENV => no value => no value xdebug.dump.FILES => no value => no value xdebug.dump.GET => no value => no value xdebug.dump.POST => no value => no value xdebug.dump.REQUEST => no value => no value xdebug.dump.SERVER => no value => no value xdebug.dump.SESSION => no value => no value xdebug.dump_globals => On => On xdebug.dump_once => On => On xdebug.dump_undefined => Off => Off xdebug.file_link_format => no value => no value xdebug.filename_format => no value => no value xdebug.force_display_errors => Off => Off xdebug.force_error_reporting => 0 => 0 xdebug.gc_stats_enable => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.gc_stats_output_dir => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.gc_stats_output_name => gcstats.%p => gcstats.%p xdebug.halt_level => 0 => 0 xdebug.idekey => no value => no value xdebug.log => no value => no value xdebug.log_level => 0 => 0 xdebug.max_nesting_level => 256 => 256 xdebug.max_stack_frames => -1 => -1 xdebug.mode => develop,debug => develop,debug xdebug.output_dir => /tmp => /tmp xdebug.overload_var_dump => (setting removed in Xdebug 3) => (setting removed in Xdebug 3) xdebug.profiler_append => Off => Off xdebug.profiler_enable => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.profiler_enable_trigger => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.profiler_enable_trigger_value => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.profiler_output_dir => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.profiler_output_name => cachegrind.out.%p => cachegrind.out.%p xdebug.remote_autostart => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.remote_connect_back => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.remote_enable => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.remote_host => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.remote_log => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.remote_log_level => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.remote_mode => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.remote_port => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.remote_timeout => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.scream => Off => Off xdebug.show_error_trace => Off => Off xdebug.show_exception_trace => Off => Off xdebug.show_local_vars => Off => Off xdebug.show_mem_delta => (setting removed in Xdebug 3) => (setting removed in Xdebug 3) xdebug.start_upon_error => default => default xdebug.start_with_request => yes => yes xdebug.trace_enable_trigger => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.trace_enable_trigger_value => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.trace_format => 0 => 0 xdebug.trace_options => 0 => 0 xdebug.trace_output_dir => (setting renamed in Xdebug 3) => (setting renamed in Xdebug 3) xdebug.trace_output_name => trace.%c => trace.%c xdebug.trigger_value => no value => no value xdebug.use_compression => 0 => 0 xdebug.var_display_max_children => 128 => 128 xdebug.var_display_max_data => 512 => 512 xdebug.var_display_max_depth => 3 => 3
已尝试方案
- 切换Xdebug和pcov两种覆盖率生成方式,结果一致
- 修改
phpunit.xml的<coverage>节点,添加modules/*/src目录 - 调整测试命名空间、
bootstrap.php位置 - 验证
bootstrap.php已引入vendor自动加载,autoload_static.php包含模块文件及命名空间(测试可正常执行)
排查方向与解决方案
1. Xdebug模式缺失coverage
当前Xdebug全局配置中xdebug.mode = develop,debug,未包含coverage模式。虽执行命令时添加了-dxdebug.mode=coverage,但建议修改全局配置确保覆盖:
xdebug.mode = develop,debug,coverage
2. PHPUnit路径通配符解析问题
尝试将模块目录明确列出,替代通配符写法:
<coverage> <include> <directory suffix=".php">src</directory> <directory suffix=".php">modules/Airport/src</directory> <directory suffix=".php">modules/OtherModule/src</directory> </include> </coverage>
3. 模块命名空间映射错误
检查模块composer.json的autoload配置,确保命名空间与目录正确映射,例如Airport模块:
{ "autoload": { "psr-4": { "Module\\Airport\\": "src/" } }, "autoload-dev": { "psr-4": { "Module\\Airport\\Tests\\": "tests/" } } }
执行composer dump-autoload更新自动加载规则。
4. Symfony Kernel加载限制
若模块包含独立Bundle,需在项目Kernel的registerBundles方法中加载对应Bundle,确保模块代码被框架实例化追踪。
5. 清理PHPUnit缓存
执行命令清理缓存后重新生成覆盖率:
php vendor/bin/phpunit --clear-cache
同时删除旧的覆盖率输出目录,避免缓存干扰。
内容的提问来源于stack exchange,提问作者Thomas Lavigne
相关产品推荐
相关产品推荐

