Testing in Banking & Payments
Overviewโ
Testing in banking is high-stakes โ a defect in a payment system can result in customer funds lost, duplicate payments, regulatory breaches, or system outages. As a result, banking teams invest heavily in testing practices, environments, and tooling.
Why Testing Is Critical in Paymentsโ
| Risk | Consequence |
|---|---|
| Duplicate payment bug | Double-debit of customer account |
| Wrong amount calculation | Financial loss / customer complaint |
| Sanctions screening bypass | Regulatory breach (massive fine) |
| Missing debit reversal | Funds not returned after rejection |
| Settlement amount mismatch | Reconciliation failure |
| Race condition in concurrent posts | Incorrect balance |
| Idempotency failure | Same payment processed twice on retry |
Test Types in Payment Systemsโ
1. Unit Testsโ
Test individual classes/methods in isolation.
@Test
void shouldCalculateNetSettlementAmount() {
// Given
BigDecimal gross = new BigDecimal("10000.00");
BigDecimal fee = new BigDecimal("15.00");
// When
BigDecimal net = feeCalculator.calculateNet(gross, fee);
// Then
assertThat(net).isEqualByComparingTo("9985.00");
}
@Test
void shouldRejectPaymentWhenInsufficientFunds() {
// Given
Account account = Account.withBalance("100.00");
// When / Then
assertThatThrownBy(() ->
paymentService.debit(account, new BigDecimal("200.00")))
.isInstanceOf(InsufficientFundsException.class);
}
2. Integration Testsโ
Test the interaction between components โ e.g., payment service + database + CBS.
@SpringBootTest
@Testcontainers
class PaymentRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
@Test
void shouldPersistAndRetrievePaymentByEndToEndId() {
PaymentOrder order = buildTestPayment("E2E-001");
repository.save(order);
Optional<PaymentOrder> found = repository.findByEndToEndId("E2E-001");
assertThat(found).isPresent();
assertThat(found.get().getAmount()).isEqualByComparingTo("500.00");
}
}
3. End-to-End (E2E) Testsโ
Test the full payment flow from instruction to posting.
@Test
void fullNppPaymentFlowShouldCompleteSuccessfully() {
// Given: a payment instruction
PaymentRequest request = PaymentRequest.builder()
.debtorBsb("062-000").debtorAccount("11111111")
.creditorBsb("032-000").creditorAccount("22222222")
.amount(new BigDecimal("500.00"))
.endToEndId("E2E-TEST-001")
.build();
// When: submitted
String paymentId = paymentClient.submit(request);
// Then: payment should be SETTLED within 30 seconds
await().atMost(30, SECONDS).until(() ->
paymentRepository.findById(paymentId)
.map(p -> p.getStatus() == SETTLED)
.orElse(false)
);
// And: debtor account debited
assertThat(accountService.getBalance("11111111"))
.isEqualByComparingTo("500.00"); // starting balance was 1000
// And: creditor account credited
assertThat(accountService.getBalance("22222222"))
.isEqualByComparingTo("500.00");
}
4. Contract Testsโ
In a microservices landscape, contract tests verify that a service's API matches what its consumers expect โ critical in payments where many services exchange ISO 20022 messages.
// Pact consumer contract test
@ExtendWith(PactConsumerTestExt.class)
class Pacs008ConsumerContractTest {
@Pact(consumer = "payment-processor", provider = "npp-gateway")
RequestResponsePact createPact(PactDslWithProvider builder) {
return builder
.given("NPP gateway is available")
.uponReceiving("a valid pacs.008 message")
.path("/npp/payments")
.method("POST")
.body(validPacs008Json())
.willRespondWith()
.status(200)
.body(pacs002AcceptedJson())
.toPact();
}
}
5. Performance / Load Testsโ
Payments must handle peak volumes (e.g., Monday morning payroll).
Tools: k6, Apache JMeter, Gatling
Test scenarios:
โโโ Steady load: 100 TPS sustained for 1 hour
โโโ Ramp up: 0 โ 500 TPS over 10 minutes
โโโ Spike: Sudden 5x load increase
โโโ Soak test: 50 TPS for 12 hours (memory leaks, etc.)
Key metrics:
โโโ P99 latency < 500ms (NPP must be < 15s end-to-end)
โโโ Zero errors under normal load
โโโ Graceful degradation under extreme load
6. Chaos / Resilience Testsโ
Deliberately break things to verify the system handles failures.
Scenarios:
โโโ CBS unavailable: Does payment queue? Does it resume on recovery?
โโโ NPP network timeout: Does debit reverse? Is idempotency preserved?
โโโ Database failover: Do in-flight payments complete or fail cleanly?
โโโ Sanctions service down: Does payment halt (fail-safe)?
Test Environments in Bankingโ
Banks maintain multiple environments to manage risk:
DEV โ Developer sandbox; often mocked dependencies
โ
โผ
SIT โ System Integration Testing; real-ish services
(System Internal: CBS, fraud, sanctions all wired up
Integration) Scheme: Stubbed or vendor sandbox
Test)
โ
โผ
UAT โ User Acceptance Testing; business validates
(User Closest to production
Acceptance Scheme: Vendor-provided test environments (NPP SIT, SWIFT LAU)
Testing)
โ
โผ
PREPROD โ Final gate; identical to production config
(Staging) Only hotfixes and release candidates here
โ
โผ
PROD โ Live environment
Scheme Test Environmentsโ
Each payment scheme provides test infrastructure:
| Scheme | Test Environment |
|---|---|
| NPP | NPP SIT (System Integration Testing) environment, managed by NPPA |
| SWIFT | SWIFT LAU (Live Application Update) / Alliance Gateway test |
| BECS | AusPayNet test facility |
| RTGS/HVCS | RBA test RTGS environment |
For NPP SIT:
- Simulates full NPP message routing
- Test BSBs allocated per participant
- pacs.008 / pacs.002 / pacs.004 all testable
- PayID lookup returns test data
- Settlement is simulated (not real money)
Idempotency Testingโ
Critical in payments โ the same payment must not be processed twice if:
- Client retries due to network timeout
- Message is delivered twice by the broker
// Test idempotency
@Test
void submittingSamePaymentTwiceShouldOnlyProcessOnce() {
PaymentRequest request = buildPaymentWithId("UNIQUE-ID-001");
// Submit twice
paymentService.process(request);
paymentService.process(request); // Duplicate
// Should only have one ledger entry
List<LedgerEntry> entries = ledgerRepository
.findByEndToEndId("UNIQUE-ID-001");
assertThat(entries).hasSize(1);
// And only one payment record
long paymentCount = paymentRepository.countByEndToEndId("UNIQUE-ID-001");
assertThat(paymentCount).isEqualTo(1);
}
ISO 20022 Message Testingโ
Testing XML-based ISO 20022 messages:
@Test
void shouldProduceValidPacs008XmlAgainstXsd() throws Exception {
// Build a pacs.008
FIToFICustomerCreditTransferV10 msg = pacs008Builder.build(testOrder);
// Marshal to XML
String xml = jaxbMarshaller.marshal(msg);
// Validate against XSD schema
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schema = factory.newSchema(
new StreamSource(getClass().getResourceAsStream("/xsd/pacs.008.001.10.xsd")));
Validator validator = schema.newValidator();
assertDoesNotThrow(() ->
validator.validate(new StreamSource(new StringReader(xml))));
}
Testing Checklist for Payment Featuresโ
Before shipping any payment change:
- Happy path โ Payment completes successfully end-to-end
- Insufficient funds โ Debit rejected; no pacs.008 sent
- Invalid account โ Correct error returned; no debit posted
- Duplicate submission โ Second request rejected; no double-posting
- Sanctions match โ Payment blocked; compliance alert raised
- Network timeout โ Debit reversal triggered; no orphaned debit
- Partial return โ pacs.004 with partial amount handled correctly
- Concurrency โ Two requests for same account simultaneously โ no race condition
- Large amounts โ BigDecimal precision (never use
doublefor money) - Currency rounding โ AUD rounds to 2 decimal places; JPY to 0
- Schema validation โ ISO 20022 XML validates against XSD
- Audit log โ All events written to audit trail
Golden Rule: Never Use double for Moneyโ
// โ WRONG โ floating point precision error
double a = 0.1 + 0.2;
System.out.println(a); // 0.30000000000000004 โ WRONG
// โ
CORRECT โ use BigDecimal
BigDecimal a = new BigDecimal("0.1").add(new BigDecimal("0.2"));
System.out.println(a); // 0.3 โ CORRECT
// โ
For currency, always specify scale and rounding
BigDecimal amount = new BigDecimal("1234.567")
.setScale(2, RoundingMode.HALF_UP); // 1234.57
Useful Libraries (Java)โ
| Library | Use |
|---|---|
| JUnit 5 | Test framework |
| Mockito | Mocking dependencies |
| Testcontainers | Spin up real PostgreSQL, Kafka, Redis in tests |
| WireMock | Mock external HTTP APIs (CBS, sanctions service) |
| Awaitility | Async assertions (wait for payment to settle) |
| AssertJ | Fluent assertions |
| JAXB | ISO 20022 XML marshalling/unmarshalling |
| prowide-iso20022 | ISO 20022 message library for Java |
Related Conceptsโ
- payment_lifecycle_101.md โ What you're testing
- payment_exceptions.md โ Edge cases to test
- core_banking.md โ System under test (posting engine)
- pacs004.md โ Return handling test cases
- reconciliation.md โ Reconciliation test scenarios