PHP项目代码覆盖率与变异

wen PHP项目 7

本文目录导读:

PHP项目代码覆盖率与变异

  1. 代码覆盖率 (Code Coverage)
  2. 变异测试 (Mutation Testing)

我来详细解释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测试套件。

抱歉,评论功能暂时关闭!