本文目录导读:

我来详细解释PHP项目中的代码覆盖率和变异测试。
代码覆盖率 (Code Coverage)
什么是代码覆盖率
代码覆盖率衡量测试用例执行过程中,源代码被覆盖的比例。
覆盖率类型
// 示例代码
class Calculator {
public function add($a, $b) {
return $a + $b; // 行覆盖率
}
public function divide($a, $b) {
if ($b == 0) { // 分支覆盖率
return "error";
}
return $a / $b;
}
public function process($value) {
if ($value > 0) { // 条件覆盖率
return "positive";
} elseif ($value < 0) {
return "negative";
}
return "zero";
}
}
PHP代码覆盖率工具
PHPUnit + php-code-coverage
<!-- phpunit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit>
<filter>
<whitelist>
<directory>src</directory>
<exclude>
<directory>src/Exceptions</directory>
</exclude>
</whitelist>
</filter>
<coverage>
<include>
<directory suffix=".php">src</directory>
</include>
<report>
<html outputDirectory="coverage" />
<clover outputFile="coverage.xml" />
</report>
</coverage>
</phpunit>
运行测试并生成覆盖率报告
# 生成覆盖率报告 vendor/bin/phpunit --coverage-html coverage/ vendor/bin/phpunit --coverage-clover coverage.xml # 设置最低覆盖率 vendor/bin/phpunit --coverage-html coverage/ --coverage-text
覆盖率配置文件
<!-- phpunit.xml.dist -->
<phpunit>
<coverage>
<report>
<html outputDirectory="build/coverage"/>
<text outputFile="php://stdout"/>
</report>
<clover outputFile="build/logs/clover.xml"/>
<crap4j outputFile="build/logs/crap4j.xml" threshold="50"/>
</coverage>
</phpunit>
实际应用示例
// src/UserService.php
class UserService {
private $db;
public function __construct(Database $db) {
$this->db = $db;
}
public function createUser($name, $email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email");
}
if ($this->userExists($email)) {
return false;
}
return $this->db->insert('users', [
'name' => $name,
'email' => $email
]);
}
private function userExists($email) {
return $this->db->query(
"SELECT COUNT(*) FROM users WHERE email = ?",
[$email]
) > 0;
}
}
// tests/UserServiceTest.php
class UserServiceTest extends TestCase {
public function testCreateUserWithValidData() {
$db = $this->createMock(Database::class);
$service = new UserService($db);
$db->method('query')->willReturn(0);
$db->method('insert')->willReturn(1);
$result = $service->createUser('John', 'john@example.com');
$this->assertTrue($result);
}
public function testCreateUserWithInvalidEmail() {
$this->expectException(InvalidArgumentException::class);
$db = $this->createMock(Database::class);
$service = new UserService($db);
$service->createUser('John', 'invalid-email');
}
}
变异测试 (Mutation Testing)
什么是变异测试
变异测试通过引入小型代码变更(变异),验证测试用例能否检测到这些变更。
常见变异操作
// 原始代码
function max($a, $b) {
if ($a > $b) {
return $a;
}
return $b;
}
// 变异操作示例
function max($a, $b) {
if ($a > $b) { // 条件反转变异:if ($a < $b)
return $a; // 返回值变异:return $b;
}
return $b; // 返回值变异:return $a;
}
PHP变异测试工具
Infection (推荐)
# 安装
composer require --dev infection/infection
# 配置 infection.json
{
"source": {
"directories": ["src"]
},
"mutators": {
"@default": true,
"NumberMutator": false,
"ConditionalBoundary": false
},
"logs": {
"text": "infection.log",
"html": "infection.html",
"summary": "infection-summary.txt"
},
"minMsi": 80,
"minCoveredMsi": 90
}
运行变异测试
# 基本运行 vendor/bin/infection # 指定配置 vendor/bin/infection --configuration=infection.json # 设置线程数 vendor/bin/infection --threads=4 # 仅测试特定文件 vendor/bin/infection --filter=Calculator.php
变异测试实战
// src/OrderProcessor.php
class OrderProcessor {
public function calculateDiscount($total, $isMember) {
if ($total > 100 && $isMember) {
return $total * 0.9; // 10% discount
}
if ($total > 200) {
return $total * 0.85; // 15% discount
}
return $total;
}
}
// tests/OrderProcessorTest.php
class OrderProcessorTest extends TestCase {
public function testMemberDiscount() {
$processor = new OrderProcessor();
$result = $processor->calculateDiscount(150, true);
$this->assertEquals(135, $result); // 150 * 0.9
}
public function testNonMemberDiscount() {
$processor = new OrderProcessor();
$result = $processor->calculateDiscount(250, false);
$this->assertEquals(212.5, $result); // 250 * 0.85
}
public function testNoDiscount() {
$processor = new OrderProcessor();
$result = $processor->calculateDiscount(50, false);
$this->assertEquals(50, $result);
}
}
覆盖率与变异测试对比
| 特性 | 代码覆盖率 | 变异测试 |
|---|---|---|
| 目的 | 测试代码执行情况 | 测试测试质量 |
| 衡量标准 | 覆盖的行/分支百分比 | 检测到的变异比例 |
| 执行速度 | 快 | 慢(需要多次运行) |
| 结果解读 | 直观易懂 | 需要分析死亡/存活的变异 |
| 资源消耗 | 低 | 高 |
最佳实践
代码覆盖率目标
<!-- phpunit.xml -->
<phpunit>
<coverage>
<report>
<html outputDirectory="coverage"/>
</report>
</coverage>
<!-- 设置覆盖率阈值 -->
<groups>
<include>
<group>critical</group>
</include>
</groups>
</phpunit>
CI/CD集成
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run tests with coverage
run: |
vendor/bin/phpunit --coverage-clover=coverage.xml
vendor/bin/infection --min-msi=70 --min-covered-msi=80
提升覆盖率策略
// 测试边界值
public function testBoundaryValues() {
$processor = new OrderProcessor();
// 边界值测试
$this->assertEquals(100, $processor->calculateDiscount(100, false));
$this->assertEquals(101, $processor->calculateDiscount(101, false));
$this->assertEquals(90, $processor->calculateDiscount(100, true));
$this->assertEquals(90.9, $processor->calculateDiscount(101, true));
// 负数测试
$this->assertEquals(0, $processor->calculateDiscount(0, false));
}
代码覆盖率和变异测试是质量保证的重要工具:
- 代码覆盖率:确保测试覆盖所有代码路径
- 变异测试:验证测试用例的实际有效性
建议组合使用这两个工具来构建高质量的PHP测试套件。