Build quality gate: Surefire, Failsafe, JaCoCo, phân tích tĩnh, CI

4 câu hỏi~20 phútTừ khóa: Maven lifecycle, surefire, failsafe, *IT, Gradle test/integrationTest, JaCoCo, coverage threshold, SonarQube quality gate, SpotBugs, Checkstyle, CI pipeline, PIT mutation testing

Tiếp đi: build quality gate. Repo shop-app giờ có cả unit test lẫn Testcontainers, mvn test mất 8 phút, và hay có người merge code không có test.

Hai việc cần làm: tách test nhanh/chậm vào đúng phase của build, và đặt cổng tự động trong CI để PR không đạt thì không merge được.

1. Bài toán

2. Vấn đề

3. Giải pháp

  1. Surefire: unit test (*Test) ở phase test. Failsafe: IT (*IT) ở integration-test + verify.
  2. JaCoCo: đo coverage, check fail build nếu dưới ngưỡng.
  3. Phân tích tĩnh: Checkstyle (style), SpotBugs (bug pattern bytecode), SonarQube (tổng hợp, quality gate trên code mới).
  4. CI gate: PR bắt buộc pipeline xanh + Sonar gate pass (branch protection).
  5. PIT: mutation testing để đo test có thật sự assert hay không.

4. Code từng bước

Bước 1: Plugin Maven

<build><plugins>
  <plugin>
    <artifactId>maven-surefire-plugin</artifactId>   <!-- mặc định: **/*Test.java -->
  </plugin>
  <plugin>
    <artifactId>maven-failsafe-plugin</artifactId>   <!-- mặc định: **/*IT.java -->
    <executions><execution>
      <goals><goal>integration-test</goal><goal>verify</goal></goals>
    </execution></executions>
  </plugin>
  <plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.12</version>
    <executions>
      <execution><id>agent</id><goals><goal>prepare-agent</goal></goals></execution>
      <execution><id>agent-it</id><goals><goal>prepare-agent-integration</goal></goals></execution>
      <execution><id>report</id><phase>verify</phase><goals><goal>report</goal></goals></execution>
      <execution>
        <id>check</id><phase>verify</phase><goals><goal>check</goal></goals>
        <configuration><rules><rule>
          <element>BUNDLE</element>
          <limits><limit>
            <counter>LINE</counter><value>COVEREDRATIO</value><minimum>0.75</minimum>
          </limit></limits>
        </rule></rules>
        <excludes><exclude>**/dto/**</exclude><exclude>**/*Application.class</exclude></excludes>
        </configuration>
      </execution>
    </executions>
  </plugin>
  <plugin>
    <groupId>com.github.spotbugs</groupId><artifactId>spotbugs-maven-plugin</artifactId>
    <executions><execution><goals><goal>check</goal></goals></execution></executions>
  </plugin>
  <plugin>
    <artifactId>maven-checkstyle-plugin</artifactId>
    <configuration><configLocation>checkstyle.xml</configLocation><failOnViolation>true</failOnViolation></configuration>
    <executions><execution><phase>validate</phase><goals><goal>check</goal></goals></execution></executions>
  </plugin>
</plugins></build>

Gradle tương đương: task test cho unit, tạo source set / JVM Test Suite integrationTest, check.dependsOn(integrationTest, jacocoTestCoverageVerification).

Bước 2: Quy ước đặt tên

OrderServiceTest.java          → surefire (mvn test)
OrderControllerTest.java       → surefire (@WebMvcTest, nhanh)
OrderEventPublisherIT.java     → failsafe (mvn verify, cần Docker)
WalletConcurrencyIT.java       → failsafe

Bước 3: CI pipeline (GitHub Actions)

name: ci
on: [pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }          # Sonar cần lịch sử để tính "new code"
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: 21, cache: maven }
      - run: ./mvnw -B verify             # checkstyle → unit → IT → jacoco check → spotbugs
      - run: ./mvnw -B sonar:sonar -Dsonar.qualitygate.wait=true
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}

Bật branch protection: job build là required check. Sonar quality gate gợi ý: coverage trên code mới ≥ 80%, 0 bug/vulnerability mới, duplication < 3%.

Bước 3b: PIT (chạy định kỳ hoặc theo module)

<plugin>
  <groupId>org.pitest</groupId><artifactId>pitest-maven</artifactId>
  <configuration>
    <targetClasses><param>com.shop.wallet.*</param></targetClasses>
    <mutationThreshold>70</mutationThreshold>
  </configuration>
  <dependencies><dependency>
    <groupId>org.pitest</groupId><artifactId>pitest-junit5-plugin</artifactId>
  </dependency></dependencies>
</plugin>
// PIT đổi  if (balance.compareTo(amount) < 0)  thành  <= 0
// Nếu không test nào đỏ → mutant "survived" → thiếu test biên: rút đúng bằng số dư.

Bước 4: Chạy

./mvnw test                 # chỉ unit, ~40s, dev chạy liên tục
./mvnw verify               # đầy đủ như CI
./mvnw verify -DskipITs     # bỏ IT khi không có Docker
./mvnw org.pitest:pitest-maven:mutationCoverage
# target/site/jacoco/index.html, target/pit-reports/

5. Cách hoạt động

Maven lifecycle
validate ─► compile ─► test ─► package ─► pre-integration-test ─► integration-test ─► post-integration-test ─► verify
  │                     │                                          │                                             │
checkstyle          surefire (*Test)                          failsafe (*IT)                       failsafe verify (fail ở đây)
                    jacoco agent                              jacoco agent-it                       jacoco check, spotbugs

Failsafe: IT fail không dừng ngay → post-integration-test vẫn dọn → verify mới đánh đỏ build

CI gate
PR ──► mvn verify ──► Sonar analysis ──► quality gate? ──► ✔ cho merge / ✘ chặn

6. So sánh

Công cụKiểm gìỞ đâu
SurefireUnit testphase test
FailsafeIntegration testintegration-test + verify
JaCoCoDòng/nhánh được chạy quaagent khi test, check ở verify
CheckstyleFormat, naming, importsource
SpotBugsBug pattern: NPE, equals/hashCode, resource leakbytecode
SonarQubeBug, smell, security hotspot, coverage, duplication; gate trên code mớiserver
PITTest có thật sự bắt lỗi khôngchạy riêng, chậm

7. Khi nào nên dùng / không nên dùng

Coverage 90% rồi thì có nghĩa là code ít bug đúng không?

Chưa chắc. Coverage chỉ nói dòng code đã được chạy qua, không nói kết quả đã được kiểm. Một test gọi walletService.deposit() mà không assert gì vẫn cho coverage cao. Coverage thấp là tín hiệu xấu chắc chắn; coverage cao chỉ là điều kiện cần. Muốn đo chất lượng assert thì dùng mutation score (PIT).

Sao không cho tất cả test chạy bằng surefire cho đơn giản, cần failsafe làm gì?

Chạy được, nhưng mất hai lợi ích. Thứ nhất, dev chạy mvn test nhanh mà không cần Docker. Thứ hai, failsafe không dừng build ngay khi IT fail, để phase post-integration-test kịp dọn môi trường (dừng app, container khởi bằng plugin), rồi verify mới báo đỏ. Với Testcontainers tự dọn thì lợi ích thứ hai nhỏ hơn, nhưng tách nhanh/chậm vẫn đáng.

Phỏng vấn hỏi về quality gate thì trả lời sao?

Q1. Surefire khác Failsafe thế nào?

🇻🇳 Tiếng Việt:

Surefire chạy unit test ở phase test và fail build ngay. Failsafe chạy integration test ở phase integration-test, không fail ngay để post-integration-test dọn môi trường, rồi mới fail ở phase verify. Quy ước tên: *Test cho surefire, *IT cho failsafe.

🇬🇧 English:

Surefire runs unit tests in the test phase and fails the build immediately. Failsafe runs integration tests in the integration-test phase without failing right away, so post-integration-test can clean up, and then fails the build in the verify phase. By convention *Test classes go to Surefire and *IT classes to Failsafe.

Q2. Bạn đặt quality gate trong CI thế nào?

🇻🇳 Tiếng Việt:

Mỗi PR chạy mvn verify: Checkstyle, unit, integration test, JaCoCo check với ngưỡng, SpotBugs. Sau đó phân tích SonarQube và chờ quality gate, tập trung vào code mới: coverage, bug và lỗ hổng mới. Pipeline là required check trong branch protection nên không đạt thì không merge được.

🇬🇧 English:

Every pull request runs mvn verify: Checkstyle, unit and integration tests, a JaCoCo check with a threshold, and SpotBugs. Then SonarQube analyses the code and we wait for the quality gate, focused on new code: coverage, new bugs and vulnerabilities. The pipeline is a required check in branch protection, so a failing PR cannot be merged.

Q3. Mutation testing là gì?

🇻🇳 Tiếng Việt:

Công cụ như PIT sửa nhỏ code (đổi điều kiện, bỏ lời gọi, đổi giá trị trả về) rồi chạy test. Nếu test vẫn xanh thì mutant sống sót, nghĩa là test chưa kiểm đủ. Nó đo chất lượng test tốt hơn coverage nhưng chậm, nên em chạy cho module lõi hoặc định kỳ.

🇬🇧 English:

A tool like PIT makes small changes to the code, such as flipping a condition, removing a call or changing a return value, and runs the tests. If the tests still pass, the mutant survived, which means the tests are not checking enough. It measures test quality better than coverage but is slow, so I run it on core modules or on a schedule.

Cách nhớ ngắn gọn


Tự kiểm tra

OrderEventPublisherIT chạy ở lệnh mvn test không?

Không. Failsafe chạy nó ở mvn verify.

Vì sao Failsafe tách goal integration-test và verify?

Để IT fail vẫn cho post-integration-test dọn môi trường, rồi verify mới đánh đỏ.

JaCoCo goal nào làm build fail?

check với rule ngưỡng (ví dụ LINE COVEREDRATIO 0.75).

SpotBugs và Checkstyle khác nhau chỗ nào?

Checkstyle kiểm style trên source; SpotBugs tìm bug pattern trên bytecode.

Mutant "survived" nghĩa là gì?

Code bị sửa mà không test nào đỏ: thiếu assert hoặc thiếu case.