Correctness Testing Strategy
Unit, Integration, Contract, and Migration Testing
Membangun testing strategy untuk JAX-RS enterprise backend: JUnit, test doubles, property-based and mutation testing, Jersey endpoint tests, Testcontainers, PostgreSQL/Kafka/Redis integration, contract tests, migration tests, concurrency, failure injection, coverage, flakiness, dan CI test architecture.
Part 041 — Unit, Integration, Contract, and Migration Testing
Test suite bukan kumpulan method beranotasi
@Test, dan code coverage bukan bukti correctness. Testing strategy adalah portfolio of evidence yang membuktikan invariant pada boundary yang tepat: pure domain logic, serialization, JAX-RS request lifecycle, database constraints, transaction behavior, message delivery, schema compatibility, migrations, authentication, concurrency, retries, dan recovery. Test yang terlalu banyak mock dapat lulus ketika sistem nyata gagal; test end-to-end yang terlalu luas dapat gagal tanpa menunjukkan penyebab. Senior engineer memilih level test berdasarkan risiko, bukan berdasarkan tren atau persentase pyramid.
Daftar Isi
- Target kompetensi
- Scope dan baseline
- Boundary dengan Part 042 dan Part 043
- Mental model testing sebagai evidence
- Risk-based test portfolio
- Test pyramid, trophy, and honeycomb
- Test taxonomy
- Test scope versus test speed
- Determinism
- Test isolation
- Repeatability dan reproducibility
- Observable outcomes
- Test naming
- Arrange-Act-Assert and Given-When-Then
- One behavior, not one assertion
- JUnit Platform, Jupiter, and engines
- JUnit 6 and Java baseline
- JUnit lifecycle
- Per-method versus per-class test instances
- Assertions
- Exception assertions
- Grouped and soft assertions
- Parameterized tests
- Dynamic tests
- Nested tests
- Tags and test suites
- Timeouts
- Parallel execution
- JUnit extensions
- Test fixtures
- Fixture builders and object mothers
- Random data and reproducible seeds
- Time and Clock
- UUID and identifier generation
- Test doubles taxonomy
- Stub
- Fake
- Mock
- Spy
- Dummy
- Mockito mental model
- Stubbing versus verification
- Strict stubbing
- Argument matching and captors
- Mocking final, static, and constructors
- Over-mocking
- Behavioral coupling
- Ports and fakes
- Unit testing domain invariants
- State-machine tests
- Money and rounding tests
- Date/time boundary tests
- Authorization decision tests
- Idempotency tests
- Retry and timeout policy tests
- Property-based testing
- Generators and shrinking
- Metamorphic properties
- Stateful property testing
- Mutation testing
- Mutation score and equivalent mutants
- Code coverage
- JaCoCo counters
- Coverage thresholds
- Coverage exclusions
- Architecture tests
- Static analysis versus executable tests
- JAX-RS resource unit tests
- JAX-RS in-process tests
- JerseyTest and runtime-specific tests
- Black-box HTTP tests
- Request matching and parameter-binding tests
- Provider and serialization tests
- Filter and interceptor tests
- ExceptionMapper tests
- Bean Validation tests
- Authentication and authorization tests
- Multipart and streaming tests
- SSE and asynchronous endpoint tests
- Testcontainers mental model
- Container lifecycle
- Static containers and singleton pattern
- Reusable containers
- Wait strategy versus startup strategy
- Dynamic host and mapped ports
- Container networks and aliases
- Image pinning
- Testcontainers cleanup and Ryuk
- CI container runtime
- PostgreSQL integration tests
- Database schema setup
- Transaction tests
- Isolation and locking tests
- Deadlock tests
- Constraint tests
- Query-plan and index tests
- MyBatis integration tests
- Migration tests
- Empty-database migration
- Upgrade-path migration
- Expand-contract compatibility tests
- Rollback and roll-forward tests
- Backfill tests
- Kafka integration tests
- Kafka duplicate, ordering, and replay tests
- Schema Registry tests
- RabbitMQ integration tests
- Redis integration tests
- Camunda workflow tests
- Cloud-service integration tests
- HTTP dependency tests with WireMock
- Fault simulation
- Toxiproxy and network failure tests
- Contract testing
- OpenAPI conformance tests
- Consumer-driven contracts
- Pact provider states
- Message contract tests
- Schema tests versus behavior contracts
- End-to-end tests
- Journey tests for quote-to-order
- End-to-end data ownership
- Security tests
- Concurrency tests
- Race-condition tests
- Deterministic concurrency coordination
- Failure injection and recovery tests
- Test data management
- Synthetic data and PII
- Cleanup and test pollution
- Parallel-test resource naming
- Flaky tests
- Flake diagnosis
- Retrying tests
- Quarantine policy
- Test-order dependence
- CI test stages
- Surefire and Failsafe boundary
- Fast feedback versus complete evidence
- Changed-code test selection
- Test reports and artifacts
- Failure-model matrix
- Debugging playbook
- Architecture patterns
- Anti-patterns
- PR review checklist
- Trade-off yang harus dipahami senior engineer
- Internal verification checklist
- Latihan verifikasi
- Ringkasan
- Referensi resmi
Target kompetensi
Setelah menyelesaikan part ini, Anda harus mampu:
- menyusun test portfolio berdasarkan business and production risk;
- membedakan unit, component, integration, contract, migration, end-to-end, and recovery tests;
- menggunakan JUnit lifecycle, parameterization, extensions, tags, suites, timeouts, and parallel execution secara aman;
- memilih stub, fake, mock, spy, or real dependency;
- mengenali over-mocking and behavior-coupled tests;
- menguji domain state machines, money, time, authorization, idempotency, and retries;
- menggunakan property-based testing and mutation testing untuk menemukan blind spots;
- menafsirkan JaCoCo coverage without treating percentages as correctness;
- menguji JAX-RS lifecycle, providers, filters, validation, security, streaming, and async behavior;
- mengoperasikan Testcontainers deterministically in local and CI environments;
- menguji PostgreSQL constraints, transactions, locks, deadlocks, MyBatis mappings, and migrations;
- menguji Kafka, RabbitMQ, Redis, workflow, and cloud adapters using real dependencies or faithful emulators;
- menggunakan WireMock and Toxiproxy for protocol and failure behavior;
- membangun OpenAPI/schema and consumer-driven contract gates;
- mengendalikan test data, parallelism, flaky tests, cleanup, and CI stages;
- melakukan PR review yang berfokus pada evidence, not test count.
Scope dan baseline
Baseline:
- Java 17+;
- JUnit 6 as current-generation conceptual baseline, while codebase may use JUnit 5 or older;
- Mockito may be used;
- Testcontainers for Java;
- Maven Surefire/Failsafe, with full Maven lifecycle deferred to Part 043;
- JAX-RS/Jersey application;
- PostgreSQL, Kafka, RabbitMQ, Redis, Camunda, and cloud adapters from prior parts;
- container runtime available locally or in CI;
- no assumption of Spring Test.
Part ini tidak mengasumsikan:
- exact testing framework versions;
- 100% coverage target;
- one test pyramid ratio;
- every dependency must use Testcontainers;
- reusable containers enabled;
- Docker socket availability in CI;
- database tests rollback after each test;
- end-to-end environment is stable;
- Pact, WireMock, jqwik, PIT, ArchUnit, or REST Assured are installed;
- internal naming conventions;
- current CSG test coverage or quality gates.
Boundary dengan Part 042 dan Part 043
| Part | Fokus |
|---|---|
| Part 041 | Functional correctness, compatibility, integration, migration, and recovery evidence |
| Part 042 | Performance, capacity, scalability, profiling, and regression measurement |
| Part 043 | Maven model, lifecycle, dependencies, plugins, multi-module build, and reproducibility |
A test that asserts p99 latency belongs primarily to Part 042, even if executed in CI.
Mental model testing sebagai evidence
A strong test states:
Given a controlled initial state,
when a behavior occurs through a meaningful boundary,
then observable invariants hold,
including failure and recovery behavior.
Risk-based test portfolio
Start with failure consequences:
| Risk | Strong evidence |
|---|---|
| Wrong pricing/rounding | unit + property + golden examples |
| Cross-tenant leak | repository + API + negative authorization integration |
| Duplicate order creation | DB constraint + idempotency integration + crash test |
| Schema break | compatibility + old/new consumer tests |
| Migration lock/outage | migration rehearsal and load behavior |
| Lost Kafka/Rabbit event | outbox/consumer crash integration |
| Redis stale authorization | expiry/invalidation/failover tests |
| Worker duplicate effect | lock-expiry/idempotency test |
| Broken route/provider | real JAX-RS runtime test |
| Vendor timeout ambiguity | WireMock/fault injection + reconciliation |
Test the expensive failure, not just easy branches.
Test pyramid, trophy, and honeycomb
These are heuristics.
Do not enforce:
70% unit
20% integration
10% E2E
without domain evidence.
For microservices, many high-value defects exist at boundaries:
- serialization;
- SQL;
- migrations;
- broker semantics;
- auth configuration;
- runtime registration;
- generated contracts.
A healthy portfolio usually contains many fast tests, a meaningful integration layer, contract gates, and a small number of end-to-end journeys.
Test taxonomy
Definitions should be local and explicit.
Suggested:
- unit: one behavior, no process/network/external engine;
- component: application component with in-memory/fake collaborators;
- integration: real interaction with one or more infrastructure components;
- contract: compatibility at service/message boundary;
- migration: schema/data upgrade behavior;
- end-to-end: deployed journey across owned systems;
- recovery: crash/failover/replay correctness;
- performance: non-functional behavior under measured workload.
Names matter less than execution guarantees.
Test scope versus test speed
Speed depends on:
- startup;
- container/image pull;
- database migration;
- test data;
- network;
- JVM forks;
- parallelism;
- cleanup.
A focused real PostgreSQL test can be faster and more valuable than a huge mocked test.
Optimize feedback without replacing evidence.
Determinism
Control:
- time;
- random seed;
- locale;
- timezone;
- ordering;
- IDs;
- network;
- external state;
- concurrency barriers;
- database state;
- image versions.
Do not use Thread.sleep() as the primary synchronization mechanism.
Test isolation
Tests must not depend on:
- execution order;
- shared mutable singleton;
- previous database rows;
- fixed broker consumer group;
- fixed ports;
- stale Redis keys;
- reused tenant IDs;
- wall clock.
Isolation can be achieved through reset, transaction, unique namespace, immutable fixture, or fresh dependency.
Repeatability dan reproducibility
Repeatable: same environment, same result.
Reproducible: another machine/CI can recreate result.
Capture:
- seed;
- versions;
- container image digest/tag;
- JDK;
- timezone;
- feature flags;
- schema revision;
- failing payload;
- logs/traces.
Observable outcomes
Prefer public/meaningful observations:
- returned value;
- domain state;
- database row;
- emitted event;
- HTTP contract;
- persisted command status.
Avoid asserting private method calls unless interaction itself is the contract.
Test naming
Example:
void approvingExpiredQuote_isRejectedWithoutPublishingOrderEvent()
Name communicates:
- scenario;
- expected outcome;
- important negative effect.
Arrange-Act-Assert and Given-When-Then
Keep phases visible.
Avoid test setup that hides the decisive input inside shared fixtures.
One behavior, not one assertion
Multiple assertions are valid when they prove one behavior atomically:
status is rejected
version unchanged
no event published
audit rejection created
Splitting can duplicate expensive setup and hide holistic invariant.
JUnit Platform, Jupiter, and engines
JUnit architecture:
- Platform launches test engines;
- Jupiter provides programming and extension model;
- other engines can coexist;
- build tools and IDEs launch through Platform.
This separation matters when discovering no tests, mixing Vintage, or configuring suites.
JUnit 6 and Java baseline
JUnit 6 is the current generation and requires Java 17.
A codebase may remain on JUnit 5; do not upgrade implicitly.
Check:
- engine;
- API;
- platform;
- Surefire provider;
- extensions;
- third-party compatibility.
JUnit lifecycle
Common callbacks:
@BeforeAll
@BeforeEach
@Test
@AfterEach
@AfterAll
Cleanup must run even after failure.
Use extension-managed resources for reusable infrastructure.
Per-method versus per-class test instances
Per-method is safer for isolation.
Per-class allows non-static @BeforeAll and shared expensive fixture, but mutable instance state can leak between tests.
Parallel execution increases risk.
Assertions
Assert business semantics with diagnostic messages.
Avoid assertTrue(complexBoolean) when separate expected/actual assertion is clearer.
Exception assertions
QuoteExpiredException error = assertThrows(
QuoteExpiredException.class,
() -> quote.approve(clock.instant()));
assertEquals(quote.id(), error.quoteId());
Also verify no forbidden side effects.
Grouped and soft assertions
assertAll reports multiple related failures.
Use for one logical result, not unrelated behaviors.
Parameterized tests
Good for:
- equivalence classes;
- boundary values;
- currencies;
- statuses;
- roles;
- timezones;
- media types.
Name cases clearly.
Dynamic tests
Useful when cases are discovered/generated at runtime.
They have lifecycle differences from ordinary methods; use carefully.
Nested tests
Organize state-machine contexts:
QuoteApprovalTest
WhenDraft
WhenExpired
WhenApprovalRequired
Do not create deeply nested fixture inheritance.
Tags and test suites
Tags enable:
- unit;
- integration;
- contract;
- slow;
- destructive;
- cloud;
- migration.
Build profiles should select explicitly.
Timeouts
A test timeout detects hangs but may leave external work running.
Use:
- operation deadline;
- JUnit timeout;
- thread dumps on timeout;
- cleanup;
- bounded containers.
Do not set universally tiny timeout that causes CI flakes.
Parallel execution
Parallel tests require:
- independent fixture;
- unique resource names;
- thread-safe extensions;
- sufficient DB/container capacity;
- no shared port;
- no static mutable mock.
Measure before enabling globally.
JUnit extensions
Extensions can manage:
- dependency injection;
- clocks;
- containers;
- temporary resources;
- logging on failure;
- retries, though retries are dangerous;
- security context.
Extensions are test infrastructure code and need tests.
Test fixtures
A fixture should express valid defaults and allow only relevant overrides.
Do not use production dumps.
Fixture builders and object mothers
Builder:
Quote quote = QuoteFixture.validQuote()
.currency("USD")
.expiresAt(now.minusSeconds(1))
.build();
Defaults must remain valid and visible.
Avoid giant object-mother with hidden coupling.
Random data and reproducible seeds
Randomness finds variants only when seed is captured.
Use bounded domain generators.
Avoid random sleeps and random infrastructure names without reporting.
Time and Clock
Inject Clock.
Test:
- exact boundary;
- before/after;
- leap day;
- DST;
- tenant business timezone;
- expiry inclusive/exclusive.
Never depend on Instant.now() in domain test.
UUID and identifier generation
Inject generator when identity affects assertions/idempotency.
Do not mock UUID merely for aesthetics if behavior does not depend on value.
Test doubles taxonomy
| Double | Purpose |
|---|---|
| Dummy | fills parameter, unused |
| Stub | returns controlled result |
| Fake | working simplified implementation |
| Mock | verifies interaction contract |
| Spy | wraps real object and observes/overrides |
Use the least powerful double.
Stub
Stub controls input from collaborator.
Do not verify every stub invocation unless interaction matters.
Fake
Examples:
- in-memory repository enforcing same uniqueness/version semantics;
- fake clock;
- fake event publisher recording events.
A fake must be tested against the real implementation contract or it can lie.
Mock
Mock is useful for:
- irreversible external command;
- verifying no event sent;
- narrow port interaction.
Mocking data/value objects is usually poor design.
Spy
Spies execute real behavior unless overridden.
They can hide complexity and cause partial-state surprises.
Use rarely.
Dummy
Use explicit harmless values, not null, when null is not contractually legal.
Mockito mental model
Mockito creates test doubles and records interactions.
It does not validate distributed behavior or framework wiring.
Use MockitoExtension with JUnit where appropriate.
Stubbing versus verification
Stubbing:
when(repository.findById(id)).thenReturn(Optional.of(quote));
Verification:
verify(outbox).append(event);
verifyNoInteractions(paymentGateway);
Verify only collaboration that is behaviorally meaningful.
Strict stubbing
Strict stubbing identifies unused/mismatched setup.
Avoid global leniency, which hides obsolete fixtures.
Argument matching and captors
Use captor when inspecting constructed command/event.
Prefer value equality when type is a proper immutable value object.
Do not use any() everywhere; it can allow wrong tenant/ID.
Mocking final, static, and constructors
Modern Mockito can mock more constructs, but capability is not design endorsement.
Static/constructor mocking often signals hidden dependency or poor composition root.
Use as migration seam, then refactor.
Over-mocking
Symptoms:
- test reproduces implementation line by line;
- every collaborator mocked;
- test passes after breaking serialization/SQL;
- refactor requires rewriting all tests;
- impossible setup.
Replace key mocks with fake or real integration.
Behavioral coupling
Bad:
verify repository called once
verify mapper called once
verify publisher called after mapper
unless ordering is actual contract.
Prefer output and durable effect.
Ports and fakes
Domain ports make focused tests possible:
interface QuoteRepository { ... }
interface OrderCommandGateway { ... }
interface QuoteEventOutbox { ... }
Fake implementations can test orchestration without network while integration tests prove adapters.
Unit testing domain invariants
Test:
- legal transitions;
- illegal transitions;
- unchanged state on rejection;
- emitted domain events;
- version increments;
- boundary values;
- invariant after every operation.
State-machine tests
Create transition matrix:
| Current | Command | Expected |
|---|---|---|
| DRAFT | submit | SUBMITTED |
| EXPIRED | submit | rejection |
| APPROVED | cancel | policy-specific |
Property: no command produces undefined state.
Money and rounding tests
Include:
- scale;
- currency mismatch;
- non-terminating division;
- tax/discount order;
- allocation sum;
- negative/zero;
- large precision.
Use exact expected values.
Date/time boundary tests
Test exact instant at expiration.
Do not only test one second far from boundary.
Authorization decision tests
Matrix:
subject role × tenant relation × resource state × action
Test deny by default and missing attributes.
Idempotency tests
Same command ID and payload:
- one effect;
- same result.
Same ID, different payload:
- conflict/integrity error.
Concurrent duplicate:
- still one durable effect.
Retry and timeout policy tests
Use deterministic fake scheduler/clock where possible.
Assert:
- retryable classification;
- max attempts;
- backoff/jitter bounds;
- remaining deadline;
- no retry for ambiguous unsafe command;
- cancellation.
Property-based testing
Property-based testing generates many inputs against invariants.
Example properties:
- allocation parts sum to total;
- serialize-deserialize preserves DTO;
- applying duplicate event is idempotent;
- quote total never changes currency;
- legal transition preserves invariant.
jqwik integrates with the JUnit Platform ecosystem.
Generators and shrinking
Generator must create meaningful domain cases.
Shrinking reduces failing input to minimal example.
Capture seed and shrunk case in CI artifact.
Metamorphic properties
When exact output is hard to predict, test relationship:
- adding zero-priced item does not change total;
- permutation of independent items preserves sum;
- serializing twice is stable;
- retrying idempotent command preserves result.
Stateful property testing
Generate sequences of commands and compare:
- model state;
- system state.
Useful for:
- state machines;
- caches;
- idempotency;
- inventory reservation;
- rate limiters.
Mutation testing
Mutation testing changes bytecode/source behavior and runs tests.
Killed mutation means tests detected change.
Survived mutation reveals weak oracle or untested behavior.
PIT is a Java mutation-testing tool.
Mutation score and equivalent mutants
Do not demand 100%.
Some mutants are equivalent or irrelevant generated code.
Use mutation analysis on critical domain code and changed code.
Code coverage
Coverage answers what executed, not what was asserted.
A test can execute a branch without validating outcome.
JaCoCo counters
JaCoCo derives counters from Java bytecode and debug information.
Counters include:
- instruction;
- branch;
- line;
- complexity;
- method;
- class.
Branch coverage is often more useful than line-only coverage.
Coverage thresholds
Use thresholds to prevent regression, not reward trivial tests.
Possible policy:
- changed-code coverage;
- critical package branch coverage;
- no global threshold for generated DTOs;
- review uncovered error paths.
Coverage exclusions
Exclude only with explicit reason:
- generated code;
- trivial configuration adapters;
- impossible instrumentation artifacts.
Do not exclude difficult code merely to improve score.
Architecture tests
Executable architecture rules can assert:
- domain does not depend on JAX-RS/JDBC;
- resources depend on application services;
- no direct SDK types in domain;
- package cycles absent;
- annotation restrictions.
Tools such as ArchUnit can help, but rules must reflect intended architecture.
Static analysis versus executable tests
Static analysis finds:
- nullability;
- dependency issues;
- security patterns;
- code smells.
Tests execute behavior.
Use both; do not label static analysis as unit tests.
JAX-RS resource unit tests
Directly calling resource method can test mapping to application service.
It does not test:
- path matching;
- providers;
- filters;
- validation integration;
- content negotiation;
- runtime lifecycle.
Keep these tests narrow.
JAX-RS in-process tests
Start actual Jersey/JAX-RS runtime in same JVM and send HTTP requests.
Proves:
- registration;
- matching;
- providers;
- filters;
- exception mapping;
- serialization.
Still may differ from production connector/container.
JerseyTest and runtime-specific tests
JerseyTest can boot Jersey test containers/providers.
Treat it as Jersey-specific, not Jakarta standard.
Verify test container implementation and lifecycle.
Black-box HTTP tests
Start packaged service and call network endpoint.
Proves more production wiring:
- actual server;
- config;
- DI;
- port;
- health;
- HTTP engine.
Costs more startup/time.
Request matching and parameter-binding tests
Test:
- overlapping templates;
- encoded path;
- repeated query params;
- missing/default;
- invalid enum/number;
- matrix/header/cookie if used;
- trailing slash;
- media-type selection.
Provider and serialization tests
Test actual registered provider:
- JSON-B/Jackson choice;
- date/time;
- decimal;
- enum unknown;
- null/absent;
- generic collections;
- XML namespace;
- malformed body;
- size limit.
Filter and interceptor tests
Verify ordering and boundary:
- authentication;
- tenant;
- authorization;
- trace;
- request/response headers;
- compression;
- entity interceptors.
Use runtime test; direct method calls miss ordering.
ExceptionMapper tests
Map:
- malformed request;
- validation;
- domain rejection;
- conflict;
- dependency timeout;
- unexpected defect.
Assert no stack trace/secret leak.
Bean Validation tests
Test:
- DTO constraints;
- cascading;
- method validation;
- groups;
- clock provider;
- violation-to-error mapping.
Do not duplicate Hibernate Validator's own implementation tests.
Authentication and authorization tests
Include:
- missing token;
- malformed token;
- wrong issuer/audience;
- expired/not-yet-valid;
- key rotation;
- wrong tenant;
- object-level denial;
- support/break-glass audit;
- no detail leakage.
Use real verifier configuration where possible.
Multipart and streaming tests
Test:
- boundary parsing;
- size;
- filename;
- MIME;
- partial body;
- client disconnect;
- checksum;
- cleanup;
- no heap buffering.
SSE and asynchronous endpoint tests
Test:
- event sequence;
- reconnect/last-event ID;
- timeout;
- cancellation;
- shutdown;
- bounded subscribers;
- context propagation.
Testcontainers mental model
Testcontainers starts disposable real services in containers controlled by test code.
It improves fidelity for protocol/storage behavior.
It does not make test automatically isolated or deterministic.
Container lifecycle
Options:
- per test;
- per class;
- suite/singleton;
- manually managed.
Trade-off startup versus isolation.
Static containers and singleton pattern
Static container can be shared across tests/classes.
Reset application data/state between tests.
Beware combining singleton patterns with extension lifecycle in ways that stop a container while later tests still reference it.
Reusable containers
Reusable containers are experimental and intended for local optimization, not a default CI guarantee.
They preserve state unless explicitly reset.
Do not rely on them for test isolation.
Wait strategy versus startup strategy
Startup strategy determines whether process/container is running.
Wait strategy determines whether service is ready for test.
Port-open alone may not mean database migration or broker cluster readiness.
Dynamic host and mapped ports
Use:
container.getHost()
container.getMappedPort(port)
Do not assume localhost or fixed ports in remote CI.
Container networks and aliases
Use shared network for multi-container topology.
Container-to-container uses alias/internal port.
Test JVM uses host/mapped port.
Image pinning
Pin known compatible image version, ideally governed digest for reproducibility.
Avoid latest.
Security updates require deliberate refresh.
Testcontainers cleanup and Ryuk
Testcontainers uses cleanup mechanisms for resources.
CI security may restrict privileged/container operations.
Do not disable cleanup casually.
CI container runtime
Verify:
- Docker/compatible environment;
- socket/remote daemon;
- network;
- image registry access;
- CPU/memory;
- nested container restrictions;
- cleanup;
- architecture.
PostgreSQL integration tests
Use real supported major version.
Test actual:
- types;
- constraints;
- isolation;
- SQL;
- query mapping;
- extensions;
- timezone;
- collation;
- functions/triggers.
H2 is not PostgreSQL.
Database schema setup
Prefer production migration tool.
This proves migration and test schema stay aligned.
Avoid hand-maintained schema-test.sql.
Transaction tests
Test commit/rollback boundaries through real service/repository.
Include crash-relevant sequence and outbox/inbox atomicity.
Isolation and locking tests
Run concurrent transactions using separate connections.
Test:
- lost update prevention;
- optimistic version;
SELECT FOR UPDATE;- serialization failure;
- lock timeout.
Deadlock tests
Deliberately acquire rows in reverse order.
Assert:
- database detects;
- one transaction aborts;
- application classifies/retries safely;
- no partial effect.
Constraint tests
Constraints are executable invariants:
- unique;
- foreign key;
- check;
- not null;
- exclusion;
- tenant composite keys.
Test expected SQL state/error mapping.
Query-plan and index tests
Plan tests can detect missing index or sequential scan on controlled data, but plans vary with statistics/version.
Use for critical queries with representative data, not brittle exact-plan string matching.
MyBatis integration tests
Test:
- parameter binding;
- result maps;
- nulls;
- dynamic SQL;
- generated keys;
- transaction participation;
- functions/procedures;
- tenant predicates;
- large data types.
Migration tests
Migration is production code.
Test:
- fresh install;
- every supported upgrade path;
- repeatability/checksum;
- mixed app versions;
- data backfill;
- lock duration;
- rollback/roll-forward.
Empty-database migration
Start from no schema and run all migrations.
Validate expected objects and application startup.
Upgrade-path migration
Restore representative old schema/data version, then migrate.
This catches assumptions hidden by fresh install.
Expand-contract compatibility tests
Run:
old app + expanded schema
new app + expanded schema
then contract cleanup only after old app gone
Test read/write compatibility.
Rollback and roll-forward tests
Many data migrations cannot safely roll back.
Test roll-forward repair, not only down scripts.
Backfill tests
Test:
- restartability;
- batching;
- checkpoint;
- duplicate execution;
- concurrent writes;
- null/invalid old data;
- performance on representative scale.
Kafka integration tests
Use real broker for:
- partitioning;
- consumer groups;
- commits;
- rebalances;
- transactions;
- schemas;
- retries.
Embedded mocks miss protocol behavior.
Kafka duplicate, ordering, and replay tests
Kill consumer:
- before effect;
- after DB commit;
- before offset commit.
Prove idempotency and ordering/gap policy.
Schema Registry tests
Test candidate compatibility and actual serializers with retained schemas.
RabbitMQ integration tests
Test:
- routing/bindings;
- confirms;
- mandatory returns;
- manual ack;
- redelivery;
- DLX;
- quorum behavior where feasible.
Redis integration tests
Test:
- TTL;
- eviction behavior;
- scripts/functions;
- transactions;
- Cluster slots;
- failover;
- serialization;
- cache fallback.
Camunda workflow tests
Test process paths and real worker/engine boundaries.
Inject duplicate jobs, incidents, timeouts, messages, cancellation, and migration/version coexistence.
Cloud-service integration tests
Use emulator for fast behavior where faithful.
Use real cloud sandbox for:
- IAM/RBAC;
- workload identity;
- TLS/private endpoint;
- service quotas;
- encryption;
- request IDs.
HTTP dependency tests with WireMock
WireMock can simulate controlled HTTP server behavior.
Test:
- status/body/headers;
- matching;
- delays;
- resets/faults;
- malformed/truncated response;
- retries;
- request signing inputs;
- verification.
Do not over-specify irrelevant headers.
Fault simulation
Important faults:
- connection refused;
- slow headers;
- slow body;
- reset after request;
- 429 with retry-after;
- 500/503;
- malformed JSON;
- duplicate response;
- redirect;
- TLS failure.
Toxiproxy and network failure tests
Testcontainers Toxiproxy module can inject network latency, cutoff, and connection disruption.
Use to test timeout/reconnect/ambiguous outcomes, not to “prove the network is reliable.”
Contract testing
Contract test proves compatibility between independent deployables.
It should not become full business journey.
OpenAPI conformance tests
Validate:
- request/response schema;
- documented status/media types;
- required headers;
- examples;
- unknown fields;
- runtime spec drift.
Schema conformance does not prove domain semantics.
Consumer-driven contracts
Consumer writes examples it needs; provider verifies them.
Pact is one implementation.
Benefits:
- catches used interaction breakage;
- supports independent deployment.
Risks:
- missing consumers;
- accidental behavior ossification;
- provider state complexity;
- no load/security guarantee.
Pact provider states
Provider state prepares deterministic data for interaction.
Do not call production-like external dependencies during verification.
State setup must be idempotent and isolated.
Message contract tests
Test event producer serialization and consumer interpretation.
Include:
- schema;
- headers;
- key;
- event type/version;
- enum unknown;
- null/default;
- duplicate/replay.
Schema tests versus behavior contracts
OpenAPI/Avro/Protobuf validation proves structure.
Consumer-driven tests prove concrete behavior.
Use both.
End-to-end tests
Use few high-value deployed journeys.
E2E is slow and diagnostically broad.
It cannot replace focused tests.
Journey tests for quote-to-order
Possible journey:
create quote
configure products
calculate price
submit
approve
create order
observe event/status
Include one failure/fallout journey.
End-to-end data ownership
Every test run needs unique tenant/account/order namespace and cleanup/expiry.
Avoid shared “test customer.”
Security tests
Test negative paths and misconfiguration:
- auth bypass;
- cross-tenant IDs;
- mass assignment;
- oversized payload;
- path traversal;
- SSRF allowlist;
- log redaction;
- secret scanning;
- dependency vulnerabilities.
Use authorized security tools/pipelines.
Concurrency tests
Concurrency tests need coordinated starts and observable invariant.
Use latches/barriers in test harness, not sleeps.
Race-condition tests
Examples:
- double approval;
- duplicate idempotency key;
- cache invalidation race;
- lock expiry;
- concurrent migration/application write;
- two consumers.
Run repeatedly and preserve failing seed/timeline.
Deterministic concurrency coordination
Use:
CountDownLatch;CyclicBarrier;- controlled fake collaborator;
- database lock;
- test hooks narrowly scoped.
Do not expose unsafe production hooks.
Failure injection and recovery tests
Inject failure at commit boundaries:
- DB commit then response loss;
- outbox publish then mark failure;
- consumer commit gap;
- worker timeout;
- Redis failover;
- process kill.
Assert final convergence.
Test data management
Test data should be:
- minimal;
- valid;
- explicit;
- versioned;
- tenant-scoped;
- generated reproducibly;
- cleaned or naturally expired.
Synthetic data and PII
Never copy production PII into ordinary test fixtures.
Synthetic data must not accidentally resemble live credentials/accounts.
Cleanup and test pollution
Strategies:
- transaction rollback;
- truncate;
- unique schema/database;
- container reset;
- TTL;
- explicit delete;
- immutable namespace.
Rollback does not test commit-time behavior.
Parallel-test resource naming
Use test-run ID:
it-${runId}-${testId}
for topics, consumer groups, buckets, tenants, and keys.
Flaky tests
A flaky test has nondeterministic outcome.
Causes:
- race;
- time;
- order;
- shared state;
- environment capacity;
- network;
- fixed port;
- eventual consistency;
- random without seed;
- weak wait.
Treat flake as defect.
Flake diagnosis
Capture:
- seed;
- timing;
- environment;
- container logs;
- thread dump;
- database locks;
- broker offsets;
- trace;
- test order;
- retries.
Repeat under controlled stress.
Retrying tests
Automatic retry can hide real defect and distort pass rate.
If used temporarily:
- report original failure;
- track flake rate;
- owner and expiry;
- never use to certify destructive migration/security correctness.
Quarantine policy
Quarantine must:
- keep visibility;
- assign owner;
- have deadline;
- not silently remove critical evidence;
- block release if risk requires.
Test-order dependence
Run random/reversed order periodically.
Clear static/shared state.
CI test stages
Example:
Performance gates are Part 042.
Surefire and Failsafe boundary
Maven convention:
- Surefire runs unit tests in
test; - Failsafe runs integration tests in
integration-testand evaluates results inverify.
Failsafe's separation allows post-integration cleanup before build failure is finalized.
Exact plugin configuration belongs to Part 043.
Fast feedback versus complete evidence
PR path:
- changed unit/property;
- focused integration;
- contract;
- schema/migration checks.
Nightly/release:
- wider integration;
- mutation;
- recovery;
- cloud sandbox;
- E2E;
- long-running tests.
Do not defer every valuable test to nightly.
Changed-code test selection
Selection can improve speed but risks missing indirect impact.
Use dependency graph plus periodic full suite.
Never skip migration/contracts solely because source package unchanged.
Test reports and artifacts
Retain:
- JUnit XML;
- logs;
- container logs;
- coverage;
- mutation report;
- contract diff;
- migration report;
- seed;
- thread dump;
- failure timeline.
Redact secrets and PII.
Failure-model matrix
| Failure | Impact | Detection | Response |
|---|---|---|---|
| Tests mirror implementation | refactor pain, weak evidence | many interaction verifications | assert behavior |
| Over-mocking infrastructure | false confidence | prod-only failures | real integration |
| Shared mutable fixture | order flakes | randomized order | isolate/reset |
| Wall-clock dependency | intermittent boundary failures | reruns/timezone | inject Clock |
| Random seed lost | unreproducible failure | CI logs missing | capture seed |
| Fixed test port | parallel conflict | bind error | dynamic port |
| H2 used as PostgreSQL proof | SQL/locking mismatch | production integration defect | real PostgreSQL |
| Migration tests only fresh DB | upgrade failure | staging/prod | old-version fixtures |
| Transaction rollback fixture | commit behavior untested | commit-time failure | explicit committed tests |
| Testcontainers port assumed localhost | remote CI failure | connection error | host/mapped port |
| Reusable container in CI | state pollution | order-dependent data | disposable lifecycle |
| Container ready only by open port | startup race | early failures | semantic wait |
| Pact over-specifies response | provider evolution blocked | contract churn | minimal consumer need |
| E2E owns shared customer | collision/flakes | cross-run state | unique namespace |
| Flaky test retried silently | false green | retry statistics | root-cause/quarantine |
| Coverage target gamed | trivial tests | mutation survives | risk/mutation review |
| Parallel tests share consumer group | missing messages | broker evidence | unique groups |
| Mock auth replaces real verifier always | config gap | deployed auth failure | verifier integration |
| Sleep-based concurrency | slow/flaky | timing sensitivity | barriers/events |
| Cleanup failure hidden | polluted environment | next-run failure | artifacts/TTL/cleanup gate |
Debugging playbook
Test passes locally, fails in CI
Compare:
- JDK;
- timezone/locale;
- CPU/memory;
- container architecture;
- Docker host;
- network;
- parallelism;
- test order;
- image availability;
- environment variables;
- filesystem case sensitivity;
- clock.
Test suite hangs
Capture thread dump.
Check:
- non-daemon executors;
- unclosed client/container;
- blocked database lock;
- future without timeout;
- consumer poll;
- Testcontainers wait;
- server shutdown;
- event-loop deadlock.
Integration test sees wrong database state
Check:
- transaction commit;
- connection/schema/search path;
- reused container;
- cleanup;
- parallel test;
- replica read;
- migration;
- tenant ID.
Testcontainers startup fails
Check:
- container runtime;
- image;
- registry credentials;
- architecture;
- mount/socket;
- wait strategy;
- logs;
- port mapping;
- cleanup service;
- CI security policy.
Consumer test times out
Check:
- topic/queue;
- unique group;
- offset start;
- schema;
- transaction visibility;
- ack/commit;
- routing;
- container readiness;
- trace/event ID.
Test is flaky only under full suite
Suspect shared static state, fixed resource names, global config, port, clock, container lifecycle, or parallel executor saturation.
Migration test fails on old snapshot
Preserve fixture and identify:
- unsupported source version;
- checksum drift;
- invalid historic data;
- non-idempotent backfill;
- lock;
- extension/version.
Architecture patterns
Domain core with fast property tests
Pure values/aggregates tested without framework.
Adapter contract suite
Every repository/messaging/cloud implementation must pass shared behavior tests.
Real infrastructure slice
JAX-RS + application + PostgreSQL/broker, with external vendors stubbed.
Contract gate
OpenAPI/schema diff plus consumer/provider verification.
Migration matrix
Fresh install and supported old-version upgrades.
Recovery harness
Kill process at named checkpoints and assert eventual state.
Unique test namespace
One run ID propagates through tenant, topics, groups, keys, and object paths.
Anti-patterns
- testing private methods;
- mock every class;
- assert only status code;
- snapshots for all JSON without semantic assertions;
- 100% line coverage as quality goal;
- no negative authorization tests;
- real production service in unit test;
Thread.sleepfor eventual result;- shared mutable
@BeforeAllfixture; - H2 as PostgreSQL substitute;
- hand-written test schema diverging from migrations;
- fixed ports;
latestcontainer images;- reusable containers in CI by default;
- integration tests skipped on every PR;
- deleting failed test evidence;
- test retry with no flake metric;
- end-to-end test for every branch;
- production PII fixtures;
- no recovery/crash tests for idempotency;
- consumer contract generated but never provider-verified.
PR review checklist
Strategy
- What production risk does each test prove?
- Is test boundary appropriate?
- Is a real dependency needed?
- Are failure and negative paths covered?
- Is observable outcome business-relevant?
- Are duplicate/retry/recovery boundaries tested?
Unit tests
- Deterministic Clock/IDs/random?
- Domain invariants and boundaries?
- No over-mocking?
- Strict stubbing?
- No implementation-order coupling?
- Property/mutation opportunity considered?
JAX-RS
- Runtime test for matching/providers/filters?
- Error mapping?
- validation?
- auth/tenant?
- content negotiation?
- streaming/size/cancellation?
Infrastructure
- Actual production-compatible version?
- Schema via migrations?
- Real transaction/lock behavior?
- Broker ack/offset/replay?
- Redis TTL/script/failover?
- Dynamic ports/names?
- deterministic wait and cleanup?
Contracts/migrations
- OpenAPI/event/proto compatibility?
- Consumer needs minimal?
- Provider verification?
- Fresh and upgrade migration?
- mixed-version compatibility?
- backfill restartability?
CI quality
- Tags/stages correct?
- Test timeout?
- Parallel-safe?
- Artifacts captured?
- No silent retry?
- Flake owner?
- Coverage interpreted correctly?
- Secrets/PII redacted?
Trade-off yang harus dipahami senior engineer
| Decision | Benefit | Cost/risk |
|---|---|---|
| Pure unit test | fast/diagnostic | misses wiring |
| Real integration | fidelity | startup/environment cost |
| Mock | precise interaction | implementation coupling |
| Fake | realistic behavior | can diverge from real adapter |
| Testcontainers per test | isolation | slow |
| Shared container | speed | reset/pollution complexity |
| Reusable container | local speed | experimental/state leakage |
| Transaction rollback | fast cleanup | misses commit behavior |
| Unique schema/database | strong isolation | setup cost |
| Contract test | deploy compatibility | not full semantics |
| E2E journey | high confidence path | broad/flaky diagnosis |
| Property testing | broad input search | generator design |
| Mutation testing | oracle strength | compute cost |
| Coverage threshold | regression signal | gaming |
| Parallel suite | speed | resource/race complexity |
| Quarantine | restores pipeline | hidden risk if unmanaged |
| Real cloud sandbox | identity fidelity | cost/flakiness |
| Emulator | speed | service-semantic gap |
Internal verification checklist
Framework and build
- JUnit version and engines.
- Mockito/version/strictness.
- Surefire/Failsafe versions and patterns.
- Test tags/profiles.
- Parallel execution.
- JUnit extensions.
- JaCoCo and coverage rules.
- Mutation/property/architecture tools.
- CI test stages and artifacts.
JAX-RS/runtime
- JerseyTest or other harness.
- Standalone/Servlet test runtime.
- Test DI/container.
- Providers/filters registration.
- Auth test keys/issuer.
- HTTP client for black-box tests.
- Streaming/multipart/SSE tests.
Infrastructure
- Testcontainers version.
- CI container runtime.
- Image source/pinning.
- PostgreSQL version/extensions.
- Kafka/Schema Registry.
- RabbitMQ.
- Redis topology.
- Camunda.
- Cloud emulators/sandboxes.
- Toxiproxy/fault tools.
Database/migrations
- Test schema setup.
- Supported upgrade snapshots.
- Liquibase/Flyway checks.
- backfill fixtures.
- transaction/locking/deadlock tests.
- MyBatis integration.
- query-plan checks.
- cleanup strategy.
Contracts and E2E
- OpenAPI conformance.
- Pact/CDC tooling.
- Event-schema compatibility.
- Provider states.
- deployed test environments.
- test tenant/data ownership.
- E2E journeys and owners.
- flake/quarantine policy.
Governance
- Coverage baseline.
- critical invariant test map.
- security test policy.
- test-data/PII policy.
- failed artifact retention.
- test ownership.
- release-blocking criteria.
- periodic recovery/game-day tests.
Latihan verifikasi
- Map ten production risks to the test boundary that best proves each.
- Replace an over-mocked repository test with a shared fake contract and PostgreSQL integration test.
- Write property tests for money allocation and state-machine transitions.
- Run mutation testing on one critical pricing/authorization package and fix weak oracles.
- Start JAX-RS with actual providers and verify malformed JSON, validation, auth, and error mapping.
- Run old-schema-to-current migration with realistic invalid legacy data.
- Kill a consumer after DB commit before offset/ack and prove idempotency.
- Simulate HTTP response loss after remote commit using WireMock/fault proxy.
- Randomize test order and parallelize with unique resource names; diagnose every failure.
- Create a flake dashboard and quarantine policy with owner and expiry.
Ringkasan
- Testing strategy is a portfolio of evidence based on risk.
- Test pyramid ratios are heuristics, not architecture laws.
- Determinism, isolation, and observable outcomes are test-design fundamentals.
- JUnit Platform and Jupiter are separate layers; JUnit 6 is the current Java-17 baseline generation.
- Use the least powerful test double; prefer behavior over implementation verification.
- Property-based tests explore invariant space; mutation tests assess oracle strength.
- Coverage reports execution, not correctness.
- Direct resource tests do not prove JAX-RS runtime behavior.
- Real runtime tests are required for matching, providers, filters, validation, and error mapping.
- Testcontainers improves infrastructure fidelity but lifecycle, readiness, image version, ports, and state isolation remain your responsibility.
- PostgreSQL tests must use PostgreSQL for transaction, type, locking, and migration behavior.
- Migration tests require fresh and supported upgrade paths.
- Contract tests complement, not replace, schemas and integration tests.
- End-to-end tests should cover a small number of high-value journeys.
- Flaky tests are defects; retries are not a permanent fix.
- Recovery tests should kill processes at transaction and acknowledgement boundaries.
- Surefire and Failsafe separate unit and integration lifecycle; full Maven design follows in Part 043.
- Exact internal test stack, quality gates, and CI policies remain Internal verification checklist.
Referensi resmi
- JUnit
- JUnit 6 User Guide
- Mockito
- Mockito Javadoc
- Testcontainers for Java
- Testcontainers JUnit 5 Quickstart
- Testcontainers Lifecycle Management
- Testcontainers Manual Lifecycle
- Testcontainers Reusable Containers
- Testcontainers Wait Strategies
- Testcontainers Networking
- Testcontainers Toxiproxy
- Maven Surefire Plugin
- Maven Failsafe Plugin
- JaCoCo Documentation
- JaCoCo Coverage Counters
- jqwik
- jqwik User Guide
- PIT Mutation Testing
- WireMock Documentation
- WireMock Fault Simulation
- Pact Documentation
- Pact JVM
You just completed lesson 41 in deepen practice. 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.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.