Skip to main content

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

RiskConsequence
Duplicate payment bugDouble-debit of customer account
Wrong amount calculationFinancial loss / customer complaint
Sanctions screening bypassRegulatory breach (massive fine)
Missing debit reversalFunds not returned after rejection
Settlement amount mismatchReconciliation failure
Race condition in concurrent postsIncorrect balance
Idempotency failureSame 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:

SchemeTest Environment
NPPNPP SIT (System Integration Testing) environment, managed by NPPA
SWIFTSWIFT LAU (Live Application Update) / Alliance Gateway test
BECSAusPayNet test facility
RTGS/HVCSRBA 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 double for 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)

LibraryUse
JUnit 5Test framework
MockitoMocking dependencies
TestcontainersSpin up real PostgreSQL, Kafka, Redis in tests
WireMockMock external HTTP APIs (CBS, sanctions service)
AwaitilityAsync assertions (wait for payment to settle)
AssertJFluent assertions
JAXBISO 20022 XML marshalling/unmarshalling
prowide-iso20022ISO 20022 message library for Java