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 的覆蓋率工具使用
  • 學會分析覆蓋率報告
  • 理解不同類型的覆蓋率指標

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

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

覆蓋率的好處:

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

Laravel Pest 覆蓋率設置

基本配置

更新 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%
    );

環境設置說明

  • PCOV:速度快,資源少,適合 CI/CD
  • Xdebug:功能全,速度慢,適合開發環境

覆蓋率類型詳解 📈

四大覆蓋率指標

  1. 語句覆蓋率(Statement Coverage)

    • 測量執行了多少程式碼語句
    • 最基本的覆蓋率類型
    • 例如:如果有 100 行程式碼,執行了 80 行,覆蓋率就是 80%
  2. 分支覆蓋率(Branch Coverage)

    • 測量執行了多少條件分支(if/else、switch)
    • 比語句覆蓋率更嚴格
    • 確保每個決策點的所有可能路徑都被測試
  3. 函數覆蓋率(Function Coverage)

    • 測量調用了多少函數
    • 找出從未被調用的「死代碼」
    • 幫助識別未使用的功能
  4. 行覆蓋率(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 (邊界條件)

改善覆蓋率策略

1. 分析覆蓋率報告

覆蓋率報告會標示:

  • 綠色:已測試的程式碼
  • 紅色:未測試的程式碼
  • ⚠️ 黃色:部分分支未測試
  • 📊 百分比數字:具體的覆蓋率數值

2. 系統化改善流程

步驟一:識別重要未覆蓋程式碼

# 產生詳細報告
pest --coverage --coverage-text --coverage-html=coverage

# 查看 HTML 報告
open coverage/index.html

步驟二:優先改善重要程式碼

  • 核心業務邏輯
  • 錯誤處理路徑
  • 複雜條件分支
  • 資料庫操作
  • API 端點

步驟三:寫測試補強覆蓋率

// 補強未覆蓋的邊界條件
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);
});

3. 覆蓋率最佳實踐

// ❌ 為了覆蓋率寫無意義測試
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);
});

4. 常見覆蓋率陷阱

避免這些錯誤:

  • 只為了提高數字而寫測試
  • 忽略測試的實際價值
  • 測試實作細節而非行為
  • 設定過高或過低的覆蓋率目標

實用技巧與進階設定

設定覆蓋率腳本

更新 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"
  }
}

覆蓋率門檻建議 📈

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

CI/CD 整合

# .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

今天學到什麼?

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

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

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

重點回顧 📝

今天的關鍵要點:

  1. 覆蓋率類型

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

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

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

明天預告 🔮

明天是第十天,我們將總結這十天學到的基礎測試概念,並為下一階段的學習做準備。你會學到如何將這些基礎概念整合運用,打造更完整的測試策略。

總結

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

記住:覆蓋率是手段而非目的。用覆蓋率來指導測試策略,而不是盲目追求數字!明天我們將總結前十天的學習,為更進階的測試技巧做準備。 🚀


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


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

尚未有邦友留言

立即登入留言