Testing strategy
| Layer |
Tooling |
What it proves |
| Unit |
JUnit 5, AssertJ |
Pure logic: router, policy tables, state transitions, deadline arithmetic |
| Contract (consumer) |
WireMock container + stubs/wallet-wiremock/mappings |
Our wallet client handles every documented wallet behaviour |
| Integration |
@SpringBootTest + Testcontainers (Postgres 17, WireMock) |
End-to-end payment flows, DB state, outbox, workers |
| Failure modes |
WireMock delays, faults and scenarios |
The fault matrix below |
| Load smoke |
k6 (tools/load/, M2-T5) |
The SLI holds and pools stay bounded when the wallet is slow |
Container tests use @Testcontainers(disabledWithoutDocker = true): they skip locally without Docker,
and they always run in CI.
Wallet stub: behaviour by account prefix
The same mappings serve docker compose and the tests. See stubs/wallet-wiremock.
| Account |
v1 POST /debits |
v1 lookup |
v2 POST /holds |
ACC-OK-* |
201 |
[] |
201 HELD |
ACC-NSF-* |
409 INSUFFICIENT_FUNDS |
[] |
409 |
ACC-DUP-* |
409 DUPLICATE_REFERENCE + existingDebitId |
[] |
— |
ACC-BLOCKED-* |
423 |
[] |
— |
ACC-DOWN-* |
503 (credits too) |
[] |
503 |
ACC-SLOW-* |
201 after 3 s (lands late) |
found |
201 after 3 s, lookup found |
ACC-LOST-* |
504 after 3 s (never lands) |
[] |
— |
| anything else |
404 ACCOUNT_NOT_FOUND |
[] |
— |
Fault matrix
| # |
Injected condition |
Assertion |
Task |
| F1 |
Wallet answers 201 after 3 s |
Till gets DECLINED WALLET_TIMEOUT in < 2.4 s, or APPROVED if the lookup finds it. The pool never saturates. |
M1-T4 |
| F2 |
Wallet 503 |
Breaker opens after the minimum number of calls. Declines take milliseconds. Nothing is queued. |
M1-T3 |
| F3 |
Wallet flaps 50/50 |
Open duration backs off. No synchronised probe storm. |
M2-T5 |
| F4 |
Debit lands, response lost (SLOW) |
Exactly one debit and exactly one compensating credit |
M1-T7 |
| F5 |
Debit never lands (LOST) |
REVERSAL_PENDING closes as REVERSED with no credit |
M1-T7 |
| F6 |
Same key, same body |
Stored response replayed. One debit. |
M1-T1, M1-T4 |
| F7 |
Same key, different body |
422. No execution. |
M1-T1 |
| F8 |
Same key, concurrent |
One executes, the other gets 409 |
M1-T1 |
| F9 |
Two concurrent payments, one account, funds for one |
Exactly one APPROVED. This is the race regression, and it needs no fault injection. |
M2-T5 |
| F10 |
Reversal before original |
Late original declined REVERSED_BEFORE_AUTHORISATION |
M1-T5 |
| F11 |
Capture response lost |
Exactly one capture |
M3-T3 |
| F12 |
Kill switch mid-flight |
In-flight holds complete. New payments use DIRECT_DEBIT. |
M3-T5 |
| F13 |
Our own DB down |
503 problem response, not a hang (Hikari 250 ms) |
M2-T5 |