Build CoreOrdered learning track

Testing with Surefire and Failsafe

Learn Maven In Action - Part 021

Production-grade testing with Maven Surefire and Failsafe: lifecycle placement, unit vs integration tests, fork strategy, parallelism, reports, flaky test control, and CI execution design.

12 min read2209 words
PrevNext
Lesson 2140 lesson track0922 Build Core
#maven#java#build-system#testing+4 more

Part 021 — Testing with Surefire and Failsafe

Target: setelah bagian ini, kamu bisa mendesain testing lifecycle Maven yang jelas: unit test cepat di test, integration test di verify, environment test dibersihkan dengan aman, hasil test terbaca oleh CI, dan build tidak rusak karena konfigurasi test yang ambigu.

Maven testing sering terlihat sederhana:

mvn test

atau:

mvn verify

Tapi di project besar, pertanyaan sebenarnya bukan “bagaimana menjalankan test”. Pertanyaannya:

  • test mana yang boleh jalan di phase apa?
  • test mana yang butuh database, broker, container, atau service eksternal?
  • kapan environment test dibuat dan dihancurkan?
  • bagaimana CI membedakan unit failure, integration failure, dan environment failure?
  • apakah parallel test aman?
  • apakah test isolation dijaga?
  • bagaimana menangani flaky test tanpa menyembunyikan bug?
  • apakah module-level test report masih bisa dibaca ketika build parallel?
  • apakah mvn integration-test aman dijalankan langsung?

Di Maven, jawaban senior-level-nya adalah: testing adalah bagian dari lifecycle contract, bukan command acak.


1. Mental Model: Test sebagai Build Gate

Build Maven production-grade punya beberapa gate.

Unit test dan integration test tidak hanya beda nama. Mereka punya lifecycle semantics berbeda.

Test TypeMaven PhasePluginShould Be Fast?External Resource?Build Gate
Unit testtestSurefireYesNocompile correctness + local logic
Integration testintegration-test + verifyFailsafeNot alwaysOften yesassembled system correctness
Contract/schema testusually test or verifySurefire/FailsafeDependsMaybeinterface compatibility
End-to-end smoke testusually external pipelinesometimes FailsafeNoYesdeployment confidence
Performance testusually separate jobcustomNoYescapacity/regression signal

Maven tidak otomatis tahu mana unit test dan mana integration test. Kamu harus membuat boundary eksplisit lewat naming convention, plugin configuration, dan lifecycle binding.


2. Surefire vs Failsafe

Surefire

maven-surefire-plugin menjalankan test di phase test.

Secara mental:

compile code
compile test code
run fast tests
fail early if unit behavior broken

Surefire cocok untuk:

  • pure unit test,
  • slice test ringan,
  • class-level behavior test,
  • fast contract test yang tidak membutuhkan environment eksternal,
  • test yang aman dijalankan setiap build lokal.

Failsafe

maven-failsafe-plugin dipakai untuk integration test di phase integration-test dan verify.

Perbedaannya kritis: Failsafe tidak langsung mem-fail build di phase integration-test, supaya phase post-integration-test tetap sempat berjalan untuk cleanup. Failure kemudian diverifikasi di verify.

Konsekuensi praktis:

# correct for integration tests
mvn verify

# dangerous if environment setup/cleanup is bound around integration-test
mvn integration-test

Memanggil integration-test langsung bisa meninggalkan resource test menggantung, karena post-integration-test dan verify belum tentu berjalan sebagai intended gate.


3. Naming Convention sebagai Routing Rule

Default convention yang umum:

File PatternMeaningPlugin
*Test.javaunit testSurefire
Test*.javaunit testSurefire
*TestCase.javaunit testSurefire
*IT.javaintegration testFailsafe
IT*.javaintegration testFailsafe
*ITCase.javaintegration testFailsafe

Rule yang bagus harus membuat developer bisa tahu tujuan test dari namanya.

Contoh:

src/test/java/com/acme/pricing/MoneyTest.java
src/test/java/com/acme/pricing/DiscountPolicyTest.java
src/test/java/com/acme/pricing/PricingRepositoryIT.java
src/test/java/com/acme/pricing/PricingApiIT.java

Jangan campur:

PricingServiceTest.java       # but starts PostgreSQL
OrderWorkflowTest.java        # but waits for Kafka
SettlementIntegrationTest.java # maybe Surefire catches it accidentally

Nama test adalah build routing metadata.


4. Baseline Parent POM untuk Testing

Di enterprise Maven, konfigurasi plugin harus dipusatkan di parent POM lewat pluginManagement.

<properties>
  <maven.surefire.version>YOUR_APPROVED_VERSION</maven.surefire.version>
  <junit.jupiter.version>YOUR_APPROVED_VERSION</junit.jupiter.version>
</properties>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.junit</groupId>
      <artifactId>junit-bom</artifactId>
      <version>${junit.jupiter.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<build>
  <pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>${maven.surefire.version}</version>
        <configuration>
          <failIfNoTests>false</failIfNoTests>
          <trimStackTrace>false</trimStackTrace>
          <useModulePath>false</useModulePath>
        </configuration>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>${maven.surefire.version}</version>
        <configuration>
          <failIfNoTests>false</failIfNoTests>
          <trimStackTrace>false</trimStackTrace>
          <useModulePath>false</useModulePath>
        </configuration>
        <executions>
          <execution>
            <id>integration-tests</id>
            <goals>
              <goal>integration-test</goal>
              <goal>verify</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </pluginManagement>
</build>

Lalu module yang membutuhkan plugin cukup mengaktifkan plugin jika perlu.

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-failsafe-plugin</artifactId>
    </plugin>
  </plugins>
</build>

Kenapa version pakai property?

Karena versi plugin test adalah policy platform. Kamu ingin satu titik kontrol saat:

  • upgrade JUnit Platform provider,
  • memperbaiki fork crash,
  • mengubah XML report behavior,
  • mengatur parallelism,
  • menyelaraskan CI report parser.

Jangan biarkan setiap service memilih versi Surefire/Failsafe sendiri tanpa alasan.


5. Dependency Test yang Bersih

Untuk JUnit 5:

<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

Untuk AssertJ:

<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <scope>test</scope>
</dependency>

Untuk Mockito:

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <scope>test</scope>
</dependency>

Jika memakai BOM internal:

<dependency>
  <groupId>com.acme.platform</groupId>
  <artifactId>acme-test-platform</artifactId>
  <version>${acme.platform.version}</version>
  <type>pom</type>
  <scope>import</scope>
</dependency>

Pattern yang sehat:

application module owns test dependencies it actually uses
parent/BOM owns versions
parent owns plugin versions/config defaults

Anti-pattern:

<!-- bad: parent injects test libraries into every module -->
<dependencies>
  <dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

Gunakan dependencyManagement, bukan inherited dependencies, kecuali memang semua module harus punya dependency itu.


6. Unit Test Boundary

Unit test yang bagus:

  • tidak butuh network,
  • tidak butuh clock real kecuali explicit,
  • tidak butuh database,
  • tidak butuh filesystem global,
  • tidak bergantung pada urutan test,
  • tidak membaca config machine developer,
  • deterministic,
  • bisa dijalankan sering.

Contoh unit test yang sehat:

class MoneyTest {
    @Test
    void rejectsDifferentCurrencyAddition() {
        Money usd = Money.of("USD", BigDecimal.TEN);
        Money eur = Money.of("EUR", BigDecimal.ONE);

        assertThatThrownBy(() -> usd.plus(eur))
            .isInstanceOf(CurrencyMismatchException.class);
    }
}

Contoh unit test yang seharusnya bukan Surefire:

class PricingRepositoryTest {
    @Test
    void savesPrice() {
        // starts real PostgreSQL container
    }
}

Nama file ini akan terlihat seperti unit test, tetapi behavior-nya integration test. Rename ke:

PricingRepositoryIT.java

7. Integration Test Boundary

Integration test boleh mahal, tapi harus jujur.

Integration test cocok untuk:

  • repository test dengan database nyata,
  • HTTP API test dengan app container,
  • Kafka/Redis/PostgreSQL integration,
  • transaction boundary,
  • serialization/deserialization boundary,
  • generated client/server compatibility,
  • workflow/process integration.

Contoh:

PricingRepositoryIT.java
PricingResourceIT.java
OrderKafkaConsumerIT.java
SettlementWorkflowIT.java

Jangan jadikan Failsafe sebagai tempat menaruh semua test lambat tanpa struktur. Integration test tetap perlu taxonomy.

IT categories:
- persistence IT
- HTTP boundary IT
- messaging IT
- workflow IT
- contract compatibility IT
- migration IT

8. Jangan Skip Test secara Sembarangan

Ada beberapa mekanisme skip.

PropertyEffectRisk
-DskipTestsskip test execution, usually still compile testsbisa melewati signal penting
-Dmaven.test.skip=trueskip compiling and running testslebih berbahaya
custom -DskipITsskip integration tests only if configuredberguna untuk local fast loop

Pattern yang baik:

<properties>
  <skipITs>false</skipITs>
</properties>

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-failsafe-plugin</artifactId>
  <configuration>
    <skipITs>${skipITs}</skipITs>
  </configuration>
</plugin>

Lalu:

# local fast feedback
mvn test

# full verification
mvn verify

# package without integration test for local experiment only
mvn package -DskipITs

Enterprise rule:

CI main branch must not use skipTests.
Release pipeline must not use skipTests.
Emergency bypass must be explicit, audited, and temporary.

9. Forking Strategy

Surefire/Failsafe bisa menjalankan test dalam forked JVM.

Kenapa fork penting?

  • isolate static/global state,
  • isolate system properties,
  • isolate JVM crashes,
  • control memory,
  • run with agent instrumentation,
  • reduce contamination between tests.

Contoh baseline:

<configuration>
  <forkCount>1</forkCount>
  <reuseForks>true</reuseForks>
  <argLine>-Xmx1024m</argLine>
</configuration>

Untuk CI besar:

<configuration>
  <forkCount>1C</forkCount>
  <reuseForks>true</reuseForks>
  <forkedProcessTimeoutInSeconds>120</forkedProcessTimeoutInSeconds>
</configuration>

1C berarti relatif terhadap jumlah CPU core. Ini powerful tapi berbahaya jika CI runner juga menjalankan Maven parallel build -T.

Multiplication Trap

mvn -T 4 verify

Dengan:

<forkCount>2</forkCount>

Potensi JVM test concurrent:

4 Maven module threads × 2 forks = 8 forked test JVMs

Jika tiap JVM pakai -Xmx2g, kamu baru meminta 16GB heap hanya untuk test forks, belum Maven, compiler, container, DB, dan OS.

Formula praktis:

max_test_jvms = maven_threads × forkCount_per_module
memory_budget = max_test_jvms × fork_heap + maven_heap + service_containers

Jangan tuning test parallelism tanpa menghitung memory.


10. Parallel Test Execution

Parallel test terlihat seperti cara mudah mempercepat build. Tapi parallelism membuka bug tersembunyi.

Parallel test aman jika test tidak berbagi:

  • static mutable state,
  • filesystem path yang sama,
  • port yang sama,
  • database schema yang sama,
  • topic/queue yang sama,
  • system property global,
  • current working directory,
  • singleton cache,
  • real clock timing assumption.

Contoh konfigurasi:

<configuration>
  <parallel>classes</parallel>
  <threadCount>4</threadCount>
</configuration>

Untuk JUnit 5, parallelism sering lebih baik dikontrol lewat JUnit Platform configuration, bukan hanya Surefire parameter.

Contoh src/test/resources/junit-platform.properties:

junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=concurrent

Rule praktis:

parallelize classes before methods
parallelize stateless tests first
never parallelize shared-container integration tests without isolation model

11. System Properties dan Environment Variables

Test sering butuh property:

<configuration>
  <systemPropertyVariables>
    <app.env>test</app.env>
    <database.schema>${project.artifactId}_${surefire.forkNumber}</database.schema>
  </systemPropertyVariables>
</configuration>

Gunakan property untuk test boundary, bukan rahasia.

Jangan:

<systemPropertyVariables>
  <db.password>prod-password</db.password>
</systemPropertyVariables>

Untuk secret test integration di CI, gunakan secret manager CI dan inject runtime environment secara controlled.

Pattern yang sehat:

POM declares variable names and safe defaults
CI injects secret values
application test harness reads environment in controlled way

12. argLine, Agents, dan JaCoCo Trap

Banyak plugin coverage memakai argLine untuk memasang Java agent.

Anti-pattern:

<argLine>-Xmx1024m</argLine>

Jika JaCoCo juga menulis argLine, konfigurasi manual bisa menimpa agent.

Pattern lebih aman:

<argLine>@{argLine} -Xmx1024m</argLine>

atau pisahkan property:

<properties>
  <test.jvm.args>-Xmx1024m</test.jvm.args>
</properties>

<configuration>
  <argLine>@{argLine} ${test.jvm.args}</argLine>
</configuration>

Checklist:

- coverage agent still attached?
- forked JVM receives expected args?
- module path/classpath behavior known?
- Java agent compatible with target JDK?
- CI report includes coverage from all modules?

13. Reports: Signal yang Dibaca Mesin

Surefire report biasanya ada di:

target/surefire-reports/

Failsafe report biasanya ada di:

target/failsafe-reports/

CI harus mengumpulkan report dari semua module:

**/target/surefire-reports/*.xml
**/target/failsafe-reports/*.xml

Senior-level rule:

A test failure that is not visible in CI UI is a build observability bug.

Report harus menjawab:

  • test mana gagal?
  • module mana?
  • phase mana?
  • apakah failure test assertion, timeout, fork crash, atau environment setup?
  • berapa lama test berjalan?
  • apakah flaky/rerun terjadi?

14. Multi-Module Testing Strategy

Dalam multi-module build:

root
├── platform-parent
├── platform-bom
├── pricing-domain
├── pricing-api
├── pricing-persistence
├── pricing-service
└── pricing-app

Tidak semua module harus punya jenis test yang sama.

ModuleUnit TestIntegration TestNotes
domainyesrarelypure logic
apiyescontract/schema maybeDTO/schema compatibility
persistenceyesyesDB IT important
serviceyesmaybemocked boundary + slice
app/deployableyesyesHTTP/container IT

Jangan paksa Failsafe di semua module jika banyak module memang tidak punya IT.

Gunakan:

<failIfNoTests>false</failIfNoTests>

atau aktifkan plugin hanya di module yang butuh.


15. Integration Test Environment Lifecycle

Contoh lifecycle dengan container plugin/custom script:

Maven phase mapping:

<execution>
  <id>start-test-environment</id>
  <phase>pre-integration-test</phase>
  <goals>
    <goal>start</goal>
  </goals>
</execution>

<execution>
  <id>stop-test-environment</id>
  <phase>post-integration-test</phase>
  <goals>
    <goal>stop</goal>
  </goals>
</execution>

Rule:

Everything started before integration-test must be stoppable after integration-test.

Dan:

Never require developers to run mvn post-integration-test manually to clean up.

16. Testcontainers dalam Maven Build

Jika memakai Testcontainers, banyak environment lifecycle dipindahkan ke test code.

Kelebihannya:

  • dekat dengan test,
  • lebih portable,
  • tidak perlu bind banyak plugin ke lifecycle,
  • cocok untuk module-level integration test.

Risikonya:

  • Docker availability menjadi implicit requirement,
  • parallel test bisa membuat banyak container,
  • image pull membuat CI lambat,
  • network flakiness terlihat seperti test failure,
  • reusable container bisa mengotori determinism.

Governance:

- tag integration tests clearly
- separate unit and IT naming
- cache Docker images in CI if allowed
- publish allowed image list
- isolate database/schema/topic per test class or fork
- make container startup logs available in CI artifacts

17. Flaky Test Strategy

Flaky test adalah test yang hasilnya tidak deterministic untuk input/build yang sama.

Sumber umum:

CauseSymptomFix Direction
real timefails around midnight/timezone/DSTinject clock
orderingfails only in suiteremove shared state
parallelismfails with -T or fork > 1isolate resource
networktimeout randomlocal test double/container
asyncassertion too earlyawait with timeout + condition
DBdirty statetransaction/schema isolation
portaddress already in userandom port allocation

Surefire/Failsafe mendukung rerun failing tests, tapi ini bukan pengganti fix.

<configuration>
  <rerunFailingTestsCount>1</rerunFailingTestsCount>
</configuration>

Policy yang sehat:

rerun = diagnostic stabilizer
not = permanent acceptance mechanism

Flaky test harus punya SLA:

- quarantine allowed only with issue link
- owner required
- expiration required
- release-blocking tests cannot be silently quarantined

18. Timeout Strategy

Test tanpa timeout bisa menggantung CI.

Contoh JUnit:

@Test
@Timeout(5)
void completesQuickly() {
    service.calculate();
}

Contoh fork timeout:

<configuration>
  <forkedProcessTimeoutInSeconds>120</forkedProcessTimeoutInSeconds>
  <forkedProcessExitTimeoutInSeconds>30</forkedProcessExitTimeoutInSeconds>
</configuration>

Bedakan:

TimeoutLevelPurpose
test method timeouttest frameworkdetect hanging behavior
fork timeoutSurefire/Failsafekill stuck JVM
CI job timeoutpipelineprevent infra waste
container startup timeoutenvironmentdetect dependency readiness issue

Timeout yang terlalu pendek membuat flaky. Timeout yang tidak ada membuat pipeline rentan hang.


19. Include/Exclude Pattern

Jika convention default tidak cukup:

<configuration>
  <includes>
    <include>**/*Test.java</include>
    <include>**/*Spec.java</include>
  </includes>
  <excludes>
    <exclude>**/*IT.java</exclude>
  </excludes>
</configuration>

Untuk Failsafe:

<configuration>
  <includes>
    <include>**/*IT.java</include>
    <include>**/*ITCase.java</include>
  </includes>
</configuration>

Tapi jangan terlalu kreatif. Convention yang terlalu banyak membuat developer bingung.

Lebih baik:

*Test = Surefire
*IT = Failsafe

Daripada:

*Spec, *Feature, *Scenario, *Journey, *E2E, *Component, *Functional, *Slow

Kecuali kamu benar-benar punya taxonomy dan CI routing yang jelas.


20. Test Categories, Tags, dan Groups

JUnit 5 tag:

@Tag("slow")
class PricingRepositoryIT {
}

Surefire/Failsafe bisa dikonfigurasi untuk include/exclude groups/tags tergantung provider.

Contoh konsep:

<configuration>
  <groups>fast</groups>
  <excludedGroups>flaky,manual</excludedGroups>
</configuration>

Gunakan tags untuk routing tambahan, bukan mengganti naming convention.

Recommended mental model:

file name decides plugin/lifecycle
annotation tag decides subset/filter

21. CI Pipeline Pattern

Pipeline sederhana:

Tapi ini compile dua kali. Lebih umum:

mvn -B -ntp verify

Untuk build besar:

Satu command Maven bisa menjalankan semua, tapi pipeline UI bisa tetap memisahkan stage secara logical dengan artifact/cache handoff.

Baseline CI command

mvn -B -ntp -Dstyle.color=always verify

Keterangan:

  • -B: batch mode,
  • -ntp: no transfer progress, log lebih bersih,
  • verify: lifecycle lengkap sampai verification,
  • jangan pakai install kecuali butuh publish ke local repo antar job,
  • jangan pakai deploy di PR validation.

22. Local Developer Workflow

Developer butuh fast loop.

# fast unit loop
mvn -q test

# test one class
mvn -Dtest=MoneyTest test

# test one method pattern
mvn -Dtest=MoneyTest#rejectsDifferentCurrencyAddition test

# integration tests only for selected module
mvn -pl pricing-persistence -am verify

# skip integration tests for local package
mvn package -DskipITs

Rule:

Local workflow may optimize speed.
CI workflow must optimize truth.

23. Partial Builds and Test Correctness

Dengan reactor:

mvn -pl pricing-service -am test

Artinya:

  • build selected module,
  • also make required upstream modules,
  • run lifecycle for selected/upstream modules.

Risk:

mvn -pl pricing-service test

Tanpa -am, Maven bisa memakai artifact lama dari local repository. Test bisa pass terhadap dependency lama.

Senior habit:

When testing a module that depends on sibling modules, use -am unless you intentionally want installed artifacts.

24. Failure Diagnostics

Case 1: Unit test not running

Checklist:

mvn -X test
mvn help:effective-pom
mvn help:describe -Dplugin=org.apache.maven.plugins:maven-surefire-plugin -Ddetail

Check:

  • naming pattern,
  • plugin included/excluded patterns,
  • test class visibility,
  • provider detection,
  • dependency on test engine,
  • skipTests,
  • profile activation,
  • module packaging.

Case 2: Integration test not running

Check:

mvn -X verify

Then inspect:

- is failsafe plugin declared under plugins, not only pluginManagement?
- are goals integration-test and verify bound?
- does test class match *IT?
- does selected lifecycle reach verify?
- is skipITs true?

Case 3: No tests to run

Could be correct for module with no tests. But in modules that should have tests, check:

src/test/java location
naming convention
packaging type
active profiles
includes/excludes

Case 4: Forked VM terminated

Common causes:

- System.exit in test/application code
- JVM crash
- OutOfMemoryError
- native library crash
- killed by CI due to memory
- incompatible javaagent

Fix path:

1. reduce fork/parallelism
2. capture dump/logs
3. increase heap only after identifying leak/usage
4. inspect argLine and javaagent
5. reproduce one test class locally

Case 5: Build hangs after failed IT

Likely environment cleanup problem.

Check:

- did you run mvn integration-test instead of mvn verify?
- is cleanup bound to post-integration-test?
- does plugin cleanup execute even after failure?
- are forked JVMs/container processes left alive?

25. Test Data Management

Unit test data:

in-memory builders
explicit fixtures
no shared global mutation

Integration test data:

schema migration baseline
isolated schema/database per class or suite
transaction rollback where possible
explicit cleanup where rollback impossible
unique topic/queue names
random port/resource names

Bad pattern:

all integration tests share acme_test database and assume execution order

Better:

schema = app_module + forkNumber + testClassHash

Example:

<systemPropertyVariables>
  <test.schema>${project.artifactId}_${surefire.forkNumber}</test.schema>
</systemPropertyVariables>

26. Contract Tests in Maven

Contract tests often sit between unit and integration.

Examples:

  • OpenAPI request/response schema compatibility,
  • JSON serialization compatibility,
  • Avro/Protobuf schema compatibility,
  • database migration compatibility,
  • generated client/server compatibility.

Where to put them?

Contract TypeRecommended Phase
pure schema validationtest
generated code compatibilitytest or verify
provider starts app/serververify
requires external broker/dbverify

Rule:

If it needs a running system boundary, use Failsafe.
If it only checks local artifacts, Surefire is enough.

27. Testing Anti-Patterns

Anti-pattern 1: All tests under Surefire

mvn test starts PostgreSQL, Kafka, Redis, and app server

Problem:

  • local build slow,
  • test phase becomes environment phase,
  • cleanup semantics weak,
  • developers skip tests.

Anti-pattern 2: All slow tests hidden behind profile

mvn verify -Pfull-tests

Problem:

  • default CI may forget profile,
  • release may skip real verification,
  • lifecycle no longer communicates truth.

Better:

verify runs required correctness gates
optional expensive suites run in separate job with explicit name

Anti-pattern 3: Parent POM injects all testing libraries

Problem:

  • dependency tree noise,
  • unused libraries everywhere,
  • version conflicts,
  • hidden coupling.

Anti-pattern 4: Rerun flaky tests forever

Problem:

  • false confidence,
  • failure signal delayed,
  • intermittent production bug ignored.

Anti-pattern 5: Tests depend on local machine config

~/.aws/credentials
localhost database
developer timezone
installed browser
fixed port 8080

Problem:

  • works on one machine,
  • fails in CI,
  • not reproducible.

28. Reference Testing Architecture

Reference rules:

1. Parent owns plugin versions and default testing policy.
2. Modules own test dependencies they actually use.
3. *Test routes to Surefire.
4. *IT routes to Failsafe.
5. CI PR validation runs mvn verify unless cost requires explicit staged split.
6. Release validation never skips test gates silently.
7. Test reports are always published.
8. Integration environment is lifecycle-bound and cleaned up.
9. Flaky tests have owner and expiration.
10. Parallelism is calculated, not guessed.

29. Practical Review Checklist

When reviewing a Maven test setup, ask:

[ ] Are Surefire and Failsafe versions pinned?
[ ] Are plugin versions managed in parent pluginManagement?
[ ] Are unit and integration tests separated by naming convention?
[ ] Is Failsafe bound to both integration-test and verify?
[ ] Does CI call mvn verify for full validation?
[ ] Are skip flags explicit and safe?
[ ] Are test reports collected from all modules?
[ ] Is fork/parallelism compatible with CI memory?
[ ] Are Java agents/argLine composed safely?
[ ] Are external resources isolated and cleaned?
[ ] Are flaky tests tracked instead of normalized?
[ ] Are module partial build commands documented?
[ ] Are test dependencies scoped test and version-managed?

30. Senior-Level Summary

Maven testing mastery is not knowing how to type mvn test. It is knowing how to preserve the meaning of every test gate.

The correct mental model:

Surefire = fast correctness gate for test phase.
Failsafe = integration correctness gate around environment lifecycle and verify phase.

The most important rule:

Unit tests should make compile-time logic trustworthy.
Integration tests should make assembled boundaries trustworthy.

And the operational rule:

Do not let test configuration lie.

If a test starts infrastructure, call it an integration test. If a test is required for release confidence, run it in CI. If a test is flaky, treat it as production signal debt. If a build skips tests, make the skip explicit and auditable.

That is how Maven testing becomes an engineering system, not a command habit.

Lesson Recap

You just completed lesson 21 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.