Build quality gate: Surefire, Failsafe, JaCoCo, phân tích tĩnh, CI
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
- Unit test (giây) và IT với Docker (phút) chạy chung trong
mvn test. - Không ai biết coverage bao nhiêu; reviewer phải tự nhắc "thiếu test".
- Bug kiểu
equalsso sánhBigDecimalbằng==, NPE tiềm ẩn lọt qua review.
2. Vấn đề
- Surefire chạy ở phase
test, dừng build ngay khi fail; nếu IT chạy ở đây thì container/app không có bướcpost-integration-testđể dọn. - Không có ngưỡng thì coverage tụt dần mà không ai thấy.
- Review bằng mắt không bền: quy tắc phải do máy kiểm.
3. Giải pháp
- Surefire: unit test (
*Test) ở phasetest. Failsafe: IT (*IT) ởintegration-test+verify. - JaCoCo: đo coverage,
checkfail build nếu dưới ngưỡng. - Phân tích tĩnh: Checkstyle (style), SpotBugs (bug pattern bytecode), SonarQube (tổng hợp, quality gate trên code mới).
- CI gate: PR bắt buộc pipeline xanh + Sonar gate pass (branch protection).
- 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 |
|---|---|---|
| Surefire | Unit test | phase test |
| Failsafe | Integration test | integration-test + verify |
| JaCoCo | Dòng/nhánh được chạy qua | agent khi test, check ở verify |
| Checkstyle | Format, naming, import | source |
| SpotBugs | Bug pattern: NPE, equals/hashCode, resource leak | bytecode |
| SonarQube | Bug, smell, security hotspot, coverage, duplication; gate trên code mới | server |
| PIT | Test có thật sự bắt lỗi không | chạy riêng, chậm |
7. Khi nào nên dùng / không nên dùng
- Ngưỡng coverage vừa phải (70–80%), áp mạnh cho code mới thay vì ép cả repo cũ.
- Không chạy theo 100% coverage: dễ sinh test không assert. PIT là cách kiểm chất lượng test.
- PIT chậm: chạy nightly hoặc cho module lõi (wallet, payment), không chạy mọi PR.
- Exclude DTO, config, class sinh code khỏi coverage.
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
- *Test → surefire → test. *IT → failsafe → verify.
- JaCoCo đo,
checkchặn. Coverage cao ≠ test tốt. - Checkstyle (style), SpotBugs (bytecode bug), Sonar (gate code mới).
- PIT: mutant sống = test yếu.
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.