0

Hướng dẫn Setup Jenkins cho hệ thống Monolith - Bài 15: Kiểm tra độ bao phủ mã nguồn (Code Coverage) trong quy trình CI/CD

Ở Bài 11 đến Bài 14, chúng ta đã thiết lập tự động chạy Unit Test và dùng SonarQube để chặn các lỗi bảo mật hay code smell. Tuy nhiên, có một "lỗ hổng quy trình" mà các đội ngũ phát triển thường mắc phải: Pipeline báo 100% Unit Test đều Pass (Xanh), SonarQube báo 0 Bug, nhưng thực tế dự án Monolith có 50.000 dòng code mà bộ Unit Test chỉ mới kiểm tra đúng... 1 hàm duy nhất dài 10 dòng!

Làm sao để đo lường xem bộ Unit Test hiện tại đã thực sự chạm tới bao nhiêu phần trăm các nhánh logic (if/else, switch/case, các luồng xử lý ngoại lệ) trong toàn bộ khối mã nguồn Monolith? Đó chính là nhiệm vụ của Code Coverage (Độ bao phủ mã nguồn).

1. Bản chất của Code Coverage và các chỉ số cốt lõi

Code Coverage là thước đo tỷ lệ phần trăm mã nguồn của ứng dụng được thực thi khi bộ Unit Test chạy qua. Trong một hệ thống Monolith phức tạp, bạn cần phân biệt rõ 3 cấp độ đo lường chính:

  1. Line Coverage (Độ phủ dòng lệnh): Trong tổng số các dòng code có thể thực thi, có bao nhiêu dòng đã được bộ test chạy qua ít nhất một lần.

  2. Branch / Condition Coverage (Độ phủ nhánh rẽ - Quan trọng nhất với Backend): Kiểm tra xem mọi nhánh true và false của câu lệnh if-else, switch-case, hoặc toán tử ba ngôi (?:) đã được test đủ các trường hợp hay chưa.

    • Ví dụ: Hàm thanh toán có nhánh if (balance >= amount) và else (throw InsufficientFundsException). Nếu bạn chỉ viết test cho trường hợp đủ tiền mà quên test trường hợp thiếu tiền, Line Coverage có thể cao nhưng Branch Coverage sẽ chỉ đạt 50%.
  3. Method / Function Coverage: Tỷ lệ các hàm/phương thức trong class đã được gọi tới trong lúc chạy test.

2. Chiến lược áp dụng Code Coverage cho hệ thống Monolith thực tế

Với một dự án Monolith lâu năm (Legacy Monolith), độ phủ code ban đầu thường rất thấp (chỉ khoảng 5% – 20%). Nếu bạn ép ngay ngưỡng 80% cho toàn bộ dự án, Pipeline sẽ bị đỏ vĩnh viễn. Chiến lược thực chiến chuẩn nhất gồm 2 nguyên tắc:

  • Tập trung vào Core Business Logic: Loại trừ (Exclude) các tệp không chứa logic nghiệp vụ ra khỏi phép tính Coverage (ví dụ: Models/Entities chỉ chứa getter/setter, các file Config, Routes, DTOs, Migrations, hoặc các Controller mỏng chỉ gọi Service). Hãy dồn sức đo lường thư mục Services/, UseCases/, Repositories/ hoặc Domain/.

  • Áp ngưỡng cứng trên Code Mới (Coverage on New Code >= 80%): Giống như triết lý của SonarQube ở Bài 13, bất kỳ tính năng mới hay hàm mới nào lập trình viên viết thêm vào Monolith bắt buộc phải đi kèm Unit Test bao phủ ít nhất 80% số dòng code mới đó thì mới được phép vượt qua Pipeline.

3. Cấu hình công cụ xuất báo cáo Coverage cho các ngôn ngữ Monolith

Để Jenkins và SonarQube đọc được dữ liệu độ phủ code, công cụ chạy test cần xuất ra các định dạng báo cáo chuẩn như Clover XML, Cobertura XML, LCOV, hoặc JaCoCo XML.

A. Đối với PHP / Laravel Monolith (Xdebug / PCOV + PHPUnit)

Trong PHP, để đo được Code Coverage, môi trường chạy test bắt buộc phải cài đặt extension PCOV (khuyên dùng trên CI/CD vì nhanh gấp 5-10 lần Xdebug) hoặc Xdebug.

  • Cấu hình chạy lệnh trong Jenkins (sử dụng biến môi trường XDEBUG_MODE=coverage nếu dùng Xdebug 3):

    Bash

    XDEBUG_MODE=coverage ./vendor/bin/phpunit \
      --log-junit reports/junit.xml \
      --coverage-clover reports/coverage-clover.xml \
      --coverage-cobertura reports/coverage-cobertura.xml
    
    

B. Đối với Golang Monolith

Golang hỗ trợ sẵn công cụ đo coverage cực kỳ mạnh mẽ ngay trong bộ cài đặt chuẩn (-coverprofile). Để hiển thị trên Jenkins hoặc đẩy sang SonarQube, bạn có thể xuất trực tiếp file coverage.out (SonarQube đọc được trực tiếp định dạng gốc của Go):

Bash

# Chạy test toàn bộ các package và xuất dữ liệu độ phủ ra file coverage.out
go test -v -race -coverprofile=reports/coverage.out -covermode=atomic ./...

# Xem nhanh tổng tỷ lệ % ngay trên màn hình Console của Jenkins
go tool cover -func=reports/coverage.out

C. Đối với Node.js / NestJS / Express Monolith (Jest)

Jest tích hợp sẵn trình đo coverage thông qua cờ --coverage. Mặc định Jest sẽ xuất ra cả định dạng lcov.info (cho SonarQube) và clover.xml trong thư mục coverage/:

Bash

npm test -- --ci --coverage --coverageDirectory="./reports/coverage"

D. Đối với Java Spring Boot Monolith (JaCoCo Maven Plugin)

Với Java, bạn khai báo jacoco-maven-plugin trong file pom.xml. Khi chạy lệnh test, JaCoCo sẽ tự động xuất file báo cáo chuẩn tại target/site/jacoco/jacoco.xml:

Bash

mvn clean test jacoco:report

4. Kết nối dữ liệu Coverage vào SonarQube và hiển thị trên Jenkins

Chúng ta sẽ kết hợp cả hai đầu: vừa đẩy báo cáo sang SonarQube để làm chốt chặn Quality Gate, vừa hiển thị biểu đồ trực quan ngay trên Jenkins Dashboard thông qua plugin Coverage Plugin (hoặc Cobertura Plugin).

Bước 1: Khai báo đường dẫn file Coverage trong sonar-project.properties

Mở lại tệp sonar-project.properties ở thư mục gốc của dự án Monolith (đã tạo ở Bài 13) và bổ sung dòng cấu hình tương ứng với ngôn ngữ của bạn:

Properties

sonar.projectKey=monolith-core-system
sonar.projectName=Monolith Core System
sonar.sourceEncoding=UTF-8
sonar.sources=app,src
sonar.tests=tests
sonar.exclusions=**/vendor/**,**/node_modules/**,**/storage/**,**/database/migrations/**

# Báo cáo kết quả chạy Unit Test
sonar.junit.reportPaths=reports/junit.xml

# --- BỔ SUNG ĐƯỜNG DẪN BÁO CÁO CODE COVERAGE (Chọn 1 dòng theo ngôn ngữ dự án) ---
# 1. Nếu là PHP (Clover XML):
sonar.php.coverage.reportPaths=reports/coverage-clover.xml

# 2. Nếu là Golang:
# sonar.go.coverage.reportPaths=reports/coverage.out

# 3. Nếu là Node.js / TypeScript (LCOV):
# sonar.javascript.lcov.reportPaths=reports/coverage/lcov.info
# sonar.typescript.lcov.reportPaths=reports/coverage/lcov.info

# 4. Nếu là Java (JaCoCo XML):
# sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml

Bước 2: Cập nhật Jenkinsfile hoàn chỉnh để đo và kiểm tra Coverage

Dưới đây là tệp Jenkinsfile đã được nâng cấp: Thực thi Unit Test kèm đo lường Coverage ở Stage 3, xuất biểu đồ lên Jenkins UI và đẩy dữ liệu sang SonarQube ở Stage 4 & 5:

Groovy

pipeline {
    agent any

    options {
        buildDiscarder(logRotator(numToKeepStr: '15'))
        timeout(time: 30, unit: 'MINUTES')
        timestamps()
    }

    environment {
        APP_NAME = "monolith-core-system"
        BUILD_VERSION = "v1.0.${env.BUILD_NUMBER}"
    }

    stages {
        stage('1. Checkout Source Code') {
            steps {
                checkout scm
            }
        }

        stage('2. Install Dependencies') {
            steps {
                sh 'composer install --no-interaction --prefer-dist'
            }
        }

        stage('3. Run Unit Tests & Generate Coverage') {
            environment {
                APP_ENV = "testing"
                DB_CONNECTION = "sqlite"
                DB_DATABASE = ":memory:"
                // Kích hoạt chế độ đo coverage cho PHP Xdebug
                XDEBUG_MODE = "coverage"
            }
            steps {
                echo "=== Chạy Unit Test và tính toán độ bao phủ mã nguồn (Code Coverage) ==="
                sh '''
                    mkdir -p reports
                    ./vendor/bin/phpunit \
                        --log-junit reports/junit.xml \
                        --coverage-clover reports/coverage-clover.xml \
                        --coverage-cobertura reports/coverage-cobertura.xml \
                        --coverage-text
                '''
            }
            post {
                always {
                    // 1. Lưu báo cáo kết quả Pass/Fail của JUnit
                    junit testResults: 'reports/junit.xml', allowEmptyResults: false
                    
                    // 2. Hiển thị biểu đồ Code Coverage trực tiếp trên Jenkins UI (Yêu cầu cài Coverage Plugin)
                    recordCoverage(
                        tools: [[parser: 'COBERTURA', pattern: 'reports/coverage-cobertura.xml']],
                        id: 'monolith-coverage',
                        name: 'Monolith Code Coverage',
                        // Có thể đặt ngưỡng cảnh báo trực tiếp trên Jenkins nếu muốn
                        qualityGates: [
                            [threshold: 60.0, metric: 'LINE', baseline: 'PROJECT', criticality: 'UNSTABLE']
                        ]
                    )
                }
            }
        }

        stage('4. SonarQube Analysis (With Coverage)') {
            steps {
                echo "=== Đẩy mã nguồn và báo cáo Coverage lên SonarQube ==="
                script {
                    def scannerHome = tool 'SonarScanner-CLI'
                    withSonarQubeEnv('SonarQube-Main-Server') {
                        sh """
                            ${scannerHome}/bin/sonar-scanner \
                            -Dsonar.projectVersion=${BUILD_VERSION}
                        """
                    }
                }
            }
        }

        stage('5. Quality Gate Check') {
            steps {
                timeout(time: 5, unit: 'MINUTES') {
                    // Nếu Coverage trên New Code < 80% (cấu hình trong SonarQube), bước này sẽ tự động đánh FAIL Pipeline!
                    waitForQualityGate abortPipeline: true
                }
            }
        }

        stage('6. Clean & Package Artifact') {
            steps {
                sh '''
                    composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
                    tar --exclude='.git' --exclude='tests' --exclude='reports' -czf ${APP_NAME}-${BUILD_VERSION}.tar.gz .
                '''
            }
        }
    }

    post {
        success {
            archiveArtifacts artifacts: "${APP_NAME}-${BUILD_VERSION}.tar.gz", fingerprint: true
        }
        always {
            sh 'rm -rf reports/ .env'
        }
    }
}

5. Cách đọc báo cáo và truy vết các dòng code "chưa được test"

Sau khi Pipeline chạy hoàn tất, bạn sẽ có một bức tranh toàn cảnh cực kỳ sắc nét ở cả hai nơi:

  1. Trên giao diện Jenkins:

    • Ngay tại trang chủ của Job xuất hiện biểu đồ Coverage Trend nằm cạnh biểu đồ Test Result Trend.

    • Khi bấm vào mục Monolith Code Coverage bên trong từng bản build, bạn sẽ thấy bảng thống kê chi tiết tỷ lệ % độ phủ theo từng thư mục (Services, Repositories, Controllers) và từng phương thức.

  2. Trên giao diện SonarQube:

    • Vào tab Measures -> Coverage: Hiển thị trực quan bằng các khối tròn (Bubble Chart) chỉ ra những file nào trong hệ thống Monolith vừa dài, vừa phức tạp lại có độ phủ test thấp nhất (đây chính là các "điểm nóng" dễ sinh bug nhất trên Production).

    • Bấm trực tiếp vào một file code bất kỳ: Các dòng được gạch vạch màu xanh lá ở lề trái là đã có Unit Test chạy qua; các dòng bị gạch vạch màu đỏ là những nhánh logic chưa từng có bài test nào chạm tới!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.