iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
Software Development

Laravel Pest TDD 實戰:從零開始的測試驅動開發系列 第 9

Day 09 - 測試覆蓋率:你的測試真的夠完整嗎? 📊

  • 分享至 

  • xImage
  •  

今天的目標

還記得昨天我們學會了例外處理測試,確保程式在錯誤情況下的穩定運行嗎?今天要面對一個更深層的問題:「我們的測試到底覆蓋了多少程式碼?」

測試覆蓋率(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: 重構技巧

學習目標

今天結束後,你將學會:

  • 理解測試覆蓋率的概念和重要性
  • 掌握 Laravel Pest 的覆蓋率工具使用
  • 學會分析覆蓋率報告
  • 理解不同類型的覆蓋率指標

📊 測試覆蓋率讓我們能夠量化測試完整性,發現潛在的測試盲點。

為什麼需要測試覆蓋率? 🎯

覆蓋率幫助我們:

  • 發現盲點:找出未測試的程式碼
  • 提升品質:確保關鍵路徑都有測試
  • 重構信心:更安全地進行重構
  • 團隊溝通:提供客觀的測試指標

Laravel Pest 覆蓋率設置

基本配置

更新 composer.json

{
  "scripts": {
    "test:coverage": "pest --coverage",
    "test:coverage-html": "pest --coverage --coverage-html=coverage"
  }
}

配置 php.ini

; 使用 PCOV(推薦)
extension=pcov.so
pcov.enabled=1

更新 pest.php

<?php

uses(Tests\TestCase::class)->in('Feature');
uses(Tests\TestCase::class)->in('Unit');

coverage()
    ->source(['app'])
    ->exclude([
        'app/Console',
        'app/Providers'
    ])
    ->thresholds(
        lines: 80,
        functions: 80,
        branches: 70
    );

覆蓋率類型詳解 📈

四大覆蓋率指標

  1. 語句覆蓋率:執行了多少程式碼語句(如 80/100 = 80%)
  2. 分支覆蓋率:測試了多少條件分支(if/else、switch)
  3. 函數覆蓋率:調用了多少函數,找出「死代碼」
  4. 行覆蓋率:執行了多少行程式碼

實際範例:計算器類別

讓我們用一個實際的例子來理解覆蓋率的重要性。

建立 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 (邊界條件)

改善覆蓋率策略

1. 分析覆蓋率報告

覆蓋率報告標示:

  • 綠色:已測試的程式碼
  • 紅色:未測試的程式碼
  • ⚠️ 黃色:部分分支未測試

2. 改善流程

# 產生詳細報告
pest --coverage --coverage-html=coverage
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);
    
    // 測試極大值情況(使用 toBeGreaterThan 避免浮點數精度問題)
    expect($calculator->getDiscount(999999.0, true))->toBeGreaterThan(199999.0);
});

3. 覆蓋率最佳實踐

// ❌ 為了覆蓋率寫無意義測試
it('covers getter', function () {
    expect((new MyClass())->getValue())->toBe(null);
});

// ✅ 寫有價值的測試
it('calculates discount correctly', function () {
    $calculator = new Calculator();
    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);
});

避免:為數字而測試、忽略實際價值、測試實作細節

實用技巧與最佳實踐

覆蓋率門檻建議 📈

  • 新專案:70-80%
  • 關鍵系統:85-90%
  • 金融/醫療:90-95%
  • 最佳平衡:85-90%

常見錯誤和解決方案

  1. 錯誤:過度追求 100% 覆蓋率
// ❌ 測試 getter/setter 無意義
it('tests simple getter', function () {
    $model = new User();
    $model->name = 'John';
    expect($model->name)->toBe('John');
});

// ✅ 測試業務邏輯
it('validates user discount eligibility', function () {
    $user = new User(['purchases' => 5]);
    expect($user->isEligibleForDiscount())->toBeTrue();
    
    $newUser = new User(['purchases' => 0]);
    expect($newUser->isEligibleForDiscount())->toBeFalse();
});
  1. 錯誤:忽略邊界條件
// ❌ 只測試正常路徑
it('processes payment', function () {
    $payment = new Payment(100.0);
    expect($payment->process())->toBeTrue();
});

// ✅ 包含邊界測試
it('handles various payment amounts', function () {
    // 最小金額
    expect((new Payment(0.01))->process())->toBeTrue();
    
    // 零金額
    expect(fn() => (new Payment(0))->process())
        ->toThrow(InvalidArgumentException::class);
    
    // 負數金額
    expect(fn() => (new Payment(-10))->process())
        ->toThrow(InvalidArgumentException::class);
    
    // 極大金額
    expect((new Payment(999999.99))->process())->toBeTrue();
});

覆蓋率迷思破解

  1. 迷思:高覆蓋率 = 高品質

    • 事實:覆蓋率只代表程式碼被執行,不代表測試品質
    • 重點在於測試邏輯的正確性和完整性
  2. 迷思:100% 覆蓋率是目標

    • 事實:某些程式碼不需要測試(如簡單配置、框架生成的程式碼)
    • 重點在於關鍵業務邏輯的測試

今天學到什麼?

透過今天的學習,我們掌握了:

  1. 測試覆蓋率的概念:了解語句、分支、函數、行覆蓋率的差異
  2. Laravel Pest 覆蓋率工具:配置和使用多種覆蓋率報告格式
  3. 覆蓋率分析技巧:識別測試盲點和改善策略
  4. 最佳實踐:平衡覆蓋率和測試品質,避免為覆蓋率而測試
  5. 實戰應用:處理複雜的分支邏輯覆蓋

測試覆蓋率讓我們能夠客觀衡量測試完整性,發現未測試的程式碼路徑,指導測試策略。記住,覆蓋率是品質的指標之一,但不是唯一指標!

重點回顧 📝

今天的關鍵要點:

  1. 覆蓋率類型

    • 語句覆蓋率:執行了多少語句
    • 分支覆蓋率:測試了多少條件路徑
    • 函數覆蓋率:調用了多少函數
    • 行覆蓋率:執行了多少行
  2. 工具配置

    • 使用 PCOV 或 Xdebug
    • 設定合理的覆蓋率閾值
    • 產生多種報告格式
  3. 最佳實踐

    • 80-90% 是合理目標
    • 關注業務邏輯測試
    • 避免無意義的測試

明天預告 🔮

明天是第十天,我們將學習重構技巧,了解如何在測試的保護下安全地改善程式碼結構。你會學到如何運用測試來支援重構,以及常見的重構模式。

總結

今天我們深入探討了測試覆蓋率,這是 TDD 過程中重要的品質指標。我們學會了如何配置和使用 Pest 的覆蓋率工具,理解了不同類型的覆蓋率指標,並知道如何合理地運用覆蓋率來改善測試品質。

記住:覆蓋率是手段而非目的。用覆蓋率來指導測試策略,而不是盲目追求數字!明天我們將學習如何在測試的保護下進行重構。 🚀


挑戰作業:檢查你現有專案的測試覆蓋率,找出至少三個未被測試的函數,為它們補充測試案例。


上一篇
Day 08 - 例外處理測試 ⚠️
下一篇
Day 10 - 重構與測試:讓程式碼持續進化 🔧
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言