還記得昨天我們學會了例外處理測試,確保程式在錯誤情況下的穩定運行嗎?今天要面對一個更深層的問題:「我們的測試到底覆蓋了多少程式碼?」
測試覆蓋率(Test Coverage)就像是程式碼的健康檢查報告,告訴你哪些程式碼被測試了,哪些還沒有。它不是萬能的,但是一個非常有用的指標,幫助我們找出測試的盲點。
我們正在第九天的基礎測試概念學習:
基礎測試概念(第 1-10 天)
├── ✅ Day 1: 環境設置與第一個測試
├── ✅ Day 2: 測試斷言
├── ✅ Day 3: TDD 紅綠重構
├── ✅ Day 4: 測試結構
├── ✅ Day 5: 生命週期
├── ✅ Day 6: 參數化測試
├── ✅ Day 7: 測試替身基礎
├── ✅ Day 8: 例外處理測試
├── 📍 Day 9: 測試覆蓋率(今天)
└── Day 10: 下階段預告
今天結束後,你將學會:
📊 測試覆蓋率讓我們能夠量化測試完整性,發現潛在的測試盲點。
覆蓋率的好處:
更新 composer.json
添加覆蓋率工具:
{
"scripts": {
"test:coverage": "pest --coverage",
"test:coverage-text": "pest --coverage --coverage-text",
"test:coverage-html": "pest --coverage --coverage-html=coverage"
},
"require-dev": {
"pestphp/pest": "^2.0",
"pestphp/pest-plugin-laravel": "^2.0"
}
}
配置 php.ini
啟用覆蓋率收集:
; 使用 PCOV(推薦,速度更快)
extension=pcov.so
pcov.enabled=1
pcov.directory=/path/to/your/project
; 或使用 Xdebug(功能更全面)
; zend_extension=xdebug.so
; xdebug.mode=coverage
更新 pest.php
設置覆蓋率參數:
<?php
uses(Tests\TestCase::class)->in('Feature');
uses(Tests\TestCase::class)->in('Unit');
coverage()
->source(['app']) // 指定要分析的目錄
->exclude([
'app/Console', // 排除控制台命令
'app/Http/Middleware', // 排除中介軟體
'app/Providers', // 排除服務提供者
'bootstrap', // 排除啟動檔案
'config', // 排除設定檔
])
->thresholds(
lines: 80, // 行覆蓋率至少 80%
functions: 80, // 函數覆蓋率至少 80%
branches: 70 // 分支覆蓋率至少 70%
);
語句覆蓋率(Statement Coverage)
分支覆蓋率(Branch Coverage)
函數覆蓋率(Function Coverage)
行覆蓋率(Line Coverage)
讓我們用一個實際的例子來理解覆蓋率的重要性。
建立 app/Services/Calculator.php
<?php
namespace App\Services;
class Calculator
{
public function divide(float $a, float $b): float
{
if ($b === 0.0) {
throw new \InvalidArgumentException('Cannot divide by zero');
}
return $a / $b;
}
public function getDiscount(float $amount, bool $isVip = false): float
{
if ($isVip) {
return $amount * 0.2;
}
return $amount > 100 ? $amount * 0.1 : 0;
}
}
建立 tests/Unit/Day09/CoverageTest.php
<?php
use App\Services\Calculator;
describe('Calculator Coverage', function () {
beforeEach(function () {
$this->calculator = new Calculator();
});
it('divides numbers correctly', function () {
expect($this->calculator->divide(10.0, 2.0))->toBe(5.0);
});
it('throws exception for division by zero', function () {
expect(fn() => $this->calculator->divide(10.0, 0.0))
->toThrow(InvalidArgumentException::class);
});
it('calculates vip discount', function () {
expect($this->calculator->getDiscount(100.0, true))->toBe(20.0);
});
it('calculates regular discount for large amount', function () {
expect($this->calculator->getDiscount(150.0, false))->toBe(15.0);
});
it('returns zero discount for small amount', function () {
expect($this->calculator->getDiscount(50.0, false))->toBe(0.0);
});
});
# 產生覆蓋率報告
composer run test:coverage
# 產生 HTML 報告
pest --coverage --coverage-html=coverage
執行後會看到:
Tests: 5 passed
Coverage: 85.7% (lines), 100% (functions), 75% (branches)
Uncovered lines:
- Calculator.php:15 (處理負數情況)
- Calculator.php:20 (邊界條件)
覆蓋率報告會標示:
步驟一:識別重要未覆蓋程式碼
# 產生詳細報告
pest --coverage --coverage-text --coverage-html=coverage
# 查看 HTML 報告
open coverage/index.html
步驟二:優先改善重要程式碼
步驟三:寫測試補強覆蓋率
// 補強未覆蓋的邊界條件
it('handles edge cases', function () {
$calculator = new Calculator();
// 測試零值情況
expect($calculator->getDiscount(0.0, true))->toBe(0.0);
// 測試負值情況
expect($calculator->getDiscount(-10.0, false))->toBe(0.0);
// 測試極大值情況
expect($calculator->getDiscount(999999.0, true))->toBe(199999.8);
});
// ❌ 為了覆蓋率寫無意義測試
it('covers getter', function () {
$obj = new MyClass();
expect($obj->getValue())->toBe(null);
});
// ✅ 寫有價值的測試,自然達到覆蓋率
it('calculates discount correctly for different scenarios', function () {
$calculator = new Calculator();
// 測試 VIP 折扣
expect($calculator->getDiscount(100.0, true))->toBe(20.0);
// 測試一般客戶無折扣
expect($calculator->getDiscount(50.0, false))->toBe(0.0);
// 測試大額一般客戶折扣
expect($calculator->getDiscount(200.0, false))->toBe(20.0);
});
避免這些錯誤:
更新 composer.json
:
{
"scripts": {
"test:coverage": "pest --coverage --min=80",
"test:coverage-detailed": "pest --coverage --coverage-text --min=80",
"test:coverage-ci": "pest --coverage --coverage-clover=coverage.xml --min=85"
}
}
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: pcov
- run: composer test:coverage-ci
- uses: codecov/codecov-action@v3
#!/bin/bash
# coverage-report.sh
pest --coverage --coverage-html=coverage --coverage-text
open coverage/index.html
透過今天的學習,我們掌握了:
測試覆蓋率讓我們能夠客觀衡量測試完整性,發現未測試的程式碼路徑,指導測試策略。記住,覆蓋率是品質的指標之一,但不是唯一指標!
今天的關鍵要點:
覆蓋率類型
工具配置
最佳實踐
明天是第十天,我們將總結這十天學到的基礎測試概念,並為下一階段的學習做準備。你會學到如何將這些基礎概念整合運用,打造更完整的測試策略。
今天我們深入探討了測試覆蓋率,這是 TDD 過程中重要的品質指標。我們學會了如何配置和使用 Pest 的覆蓋率工具,理解了不同類型的覆蓋率指標,並知道如何合理地運用覆蓋率來改善測試品質。
記住:覆蓋率是手段而非目的。用覆蓋率來指導測試策略,而不是盲目追求數字!明天我們將總結前十天的學習,為更進階的測試技巧做準備。 🚀
挑戰作業:檢查你現有專案的測試覆蓋率,找出至少三個未被測試的函數,為它們補充測試案例。