由於中途導入單元測試,為了確保測試覆蓋率能穩定的成長,每個月我們會審視當月份覆蓋率是否往目標成長。而過去的做法就是每個月執行一次,然後將數據收集起來。為了將這個覆蓋率的行為自動化,花了點時間整合到 Github Action。

Coverage Report as Comment (Clover)

從 Github marketplace 搜尋 Coverage 其實可以找到不少現成的方案。有些甚至跟單元測試工具無縫結合,可以省去自己整合的需求。

首先我挑選了 Coverage Report as Comment (Clover) ,他能夠將覆蓋率報告作為備註顯示在 comment 裡頭。具體結果類似如下:

Coverage report for commit: 0688430
File: coverage.xml

Cover ┌─────────────────────────┐ Freq.
   0% │ ██░░░░░░░░░░░░░░░░░░░░░ │  5.6%
  10% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  20% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  30% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  40% │ █░░░░░░░░░░░░░░░░░░░░░░ │  0.8%
  50% │ █░░░░░░░░░░░░░░░░░░░░░░ │  2.4%
  60% │ █░░░░░░░░░░░░░░░░░░░░░░ │  0.8%
  70% │ █░░░░░░░░░░░░░░░░░░░░░░ │  1.6%
  80% │ █░░░░░░░░░░░░░░░░░░░░░░ │  0.8%
  90% │ ████░░░░░░░░░░░░░░░░░░░ │ 11.3%
 100% │ ███████████████████████ │ 76.6%
      └─────────────────────────┘
 *Legend:* █ = Current Distribution

Summary - Lines: 90.93% | Methods: 75.71%

🤖 comment via lucassabreu/comment-coverage-clover

由於他官方範例就是利用 PHPUnit 產生 clover 格式的覆蓋率報告,所以很容易的就整合到現在的單元測試流程。

- name: Execute tests
  run: src/vendor/bin/phpunit -c src/phpunit.xml
  run: src/vendor/bin/phpunit -c src/phpunit.xml --coverage-clover=coverage.xml
  env:
    XDEBUG_MODE: coverage

- name: Coverage Report as Comment (Clover)
  uses: lucassabreu/comment-coverage-clover@main
  with:
    file: coverage.xml

Code Coverage Summary

上面的結果看起來還不錯,但想試試看其他的套件結果如何,這次我改用 Code Coverage Summary。雖然他的範例不是針對 PHPUnit,但他接受 cobertura 格式的覆蓋率檔案。PHPUnit 一樣支持。

- name: Execute tests
  run: src/vendor/bin/phpunit -c src/phpunit.xml --coverage-cobertura=coverage.xml
  env:
    XDEBUG_MODE: coverage

- name: Code Coverage Report
  uses: irongut/CodeCoverageSummary@v1.3.0
  with:
    filename: coverage.xml
    badge: true
    fail_below_min: false
    format: markdown
    hide_branch_rate: false
    hide_complexity: true
    indicators: true
    output: both
    thresholds: '50 75'

- name: Add Coverage PR Comment
  uses: marocchino/sticky-pull-request-comment@v2
  if: github.event_name == 'pull_request'
  with:
    file: coverage.xml
    recreate: true
    path: code-coverage-results.md

生成的結果類似如下:

Code Coverage

Package Line Rate Health
Console/Kernel.php 67%
Exceptions/Handler.php 50%
Http/Kernel.php 0%
Http/Middleware/Authenticate.php 100%
….
Summary 91% (2070 / 2272)

顯示在 README.md

結果都對了,但新一輪的問題是,這些結果預設是顯示在 Pull requests 裡頭作為 Comment。我不希望每次查看覆蓋率都得去翻閱之前合併請求的備註內容。這樣很難顯眼的看到當前專案的覆蓋率。

為了將內容顯示在隨時可以存取的地方,我找到一篇將結果放在 Wiki 的文章

但隨即詢問同事才知道免費的 Github team 不支援 Wiki。既然如此不如就放在 README.md 吧!完整的需求如下:

  • 只在 main 分支進行覆蓋率測試
  • 其他 pull requests 一樣執行單元測試
  • 將測試結果的文字顯示在 README.md,並且提交該變更。

我將上述的需求詢問 GPT 大神之後,得到一個粗略的版本,然後在幾次翻修後得到了我想要的版本:


name: Run tests

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  php-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.3
          tools: composer:v2
          coverage: xdebug

      - name: Run composer install
        run: composer install -d`pwd`/src -n --prefer-dist --no-interaction --no-plugins

      - name: Execute tests without coverage
        if: github.ref != 'refs/heads/main'
        run: src/vendor/bin/phpunit -c src/phpunit.xml





      ### Code Coverage
      - name: Execute tests with coverage
        if: github.ref == 'refs/heads/main'
        run: src/vendor/bin/phpunit -c src/phpunit.xml --coverage-text --only-summary-for-coverage-text | tee coverage.txt
        env:
          XDEBUG_MODE: coverage

      - name: Extract Coverage Summary
        if: github.ref == 'refs/heads/main'
        run: |
          CONTENT=$(sed -n '/^  Classes:/,/^```/p' coverage.txt)
          START="## Code Coverage Report Summary"
          NEW_CONTENT=$(printf "%s\n\n\`\`\`sql\n%s\n\`\`\`" "$START" "$CONTENT")
          awk -v new_content="$NEW_CONTENT" '
          /## Code Coverage Report Summary/ {
              in_block = 1;
          }
          in_block {
              if (/```$/) {
                  in_block = 0;
                  print new_content; # 輸出新內容
              }
              next
          }
          { print }
          ' README.md > README.tmp
          mv README.tmp README.md          

      - name: Commit and Push Changes
        if: github.ref == 'refs/heads/main'
        run: |
          git config --global user.name "github-actions[bot]"
          git config --global user.email "github-actions[bot]@users.noreply.github.com"
          git add README.md
          git commit -m "Update code coverage report summary" || echo "No changes to commit"
          git push origin ${{ github.head_ref }}          
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

為了配合上述的內容,我需要在 README.md 裡頭添加一段覆蓋率報告文字

## Code Coverage Report Summary

```sql
  Classes: XX.XX%
  Methods: XX.XX%
  Lines:   XX.XX%
` ` `

單元測試後會產生類似內容,我將內容擷取出來後,透過 awk 取代 README.md 相同的內容,改為最新的覆蓋率報告。最後結果呈現如下:

My Project

Code Coverage Report Summary

  Classes: 57.89% (55/95)
  Methods: 76.92% (190/247)
  Lines:   91.11% (2070/2272)

踩坑

在開發的過程中踩了幾個坑:

awk: newline in string $## Code Coverage Re… at source line 1

首先希望先在本機端執行一次 awk 的內容取代。但出現了如標題的錯誤,原因是因為 awk 指令在 linux 跟 MacOS 版本有差異。尤其在處理換行字元的部分。但本地端只是測試,所以我將輸出的內容去掉換行字元。先在本地端能正確模擬就好。

覆蓋率的內容不見了

將這個 action 放到另外一個專案,跑完之後內容卻只剩下如下標題:

## Code Coverage Report Summary

這是由於 README.md 的換行字元所導致,該檔案是 CRLF 儲存在 Windows 的環境,但 action 執行在 UNIX 上面採用的是 LF。這似乎會導致 awk 執行結果不正確。重新儲存檔案為 LF 字元後解決。

fatal: You are not currently on a branch.

由於開發的時候,我先在 pull request 所在的分支執行,在最終 git commit 的環節出錯,該問題可以透過添加 ref: ${{ github.head_ref }} 解決。

- name: Checkout the correct branch
  uses: actions/checkout@v4
  with:
    ref: ${{ github.head_ref }}