Testing Reality
What We Test vs. What We Say We Test
🎯 What You'll Learn
- The honest reality of testing in a financial services platform
- What gets tested, what doesn't, and why
- Practical testing frameworks and patterns actually used
- Manual testing procedures that keep the system running
- How to add effective tests to the existing codebase
Testing Reality Check
⚠️ Honest Assessment
Dione's testing coverage is pragmatic rather than comprehensive. We focus on testing what breaks often and what costs the most when it fails. Perfect is the enemy of working in financial services.
Testing Philosophy
Financial Impact First
Test payment flows and currency calculations religiously. UI quirks can wait.
Security Critical
Authentication and authorization paths are thoroughly tested.
Integration Heavy
More integration tests than unit tests - that's where the real bugs hide.
Speed Over Coverage
Fast feedback is more valuable than 100% coverage.
What We Actually Test
HIGH PRIORITY (Comprehensive Testing):
├── Payment processing logic
├── Currency conversion calculations
├── Authentication & session management
├── External service integrations
├── Database transactions
└── Security validations
MEDIUM PRIORITY (Smoke Testing):
├── Customer onboarding flows
├── KYC document processing
├── Email notifications
├── Reporting functions
└── Admin portal operations
LOW PRIORITY (Manual Testing Only):
├── UI responsiveness
├── CSS styling issues
├── Browser compatibility
├── Performance edge cases
└── Non-critical validations
Test Frameworks & Setup
Dione uses a practical mix of testing frameworks that work well with the existing Java/Spring stack.
Testing Stack
<!-- Unit Testing -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
<!-- Spring Testing -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.18.RELEASE</version>
<scope>test</scope>
</dependency>
<!-- Database Testing -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
<scope>test</scope>
</dependency>
<!-- SOAP Service Testing -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
<version>3.2.7</version>
<scope>test</scope>
</dependency>
Test Configuration
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx">
<!-- Test database configuration -->
<bean id="testDataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="org.h2.Driver"/>
<property name="url" value="jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<!-- Test entity manager -->
<bean id="testEntityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="testDataSource"/>
<property name="packagesToScan" value="com.currenciesdirect.gtg.ngop.business.entities"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
</property>
<property name="jpaProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
<prop key="hibernate.hbm2ddl.auto">create-drop</prop>
<prop key="hibernate.show_sql">false</prop>
</props>
</property>
</bean>
<!-- Mock external services -->
<bean id="mockPricingEngineService" class="org.mockito.Mockito"
factory-method="mock">
<constructor-arg value="com.currenciesdirect.gtg.ngop.business.services.PricingEngineService"/>
</bean>
<bean id="mockSalesforceService" class="org.mockito.Mockito"
factory-method="mock">
<constructor-arg value="com.currenciesdirect.gtg.ngop.business.services.SalesforceService"/>
</bean>
</beans>
Unit Testing Patterns
Unit tests in Dione focus on business logic validation, especially around financial calculations and validations.
Business Logic Testing
@RunWith(MockitoJUnitRunner.class)
public class PaymentValidationHandlerTest {
@Mock
private CustomerDao customerDao;
@Mock
private OrganizationDao organizationDao;
@InjectMocks
private PaymentValidationHandler paymentValidationHandler;
@Test
public void testValidPaymentRequest() {
// Given
PaymentRequest request = createValidPaymentRequest();
Integer customerId = 123;
Integer organizationId = 1;
when(organizationDao.getOrganizationCode(organizationId)).thenReturn("CD");
// When & Then - should not throw exception
PaymentValidationHandler.validatePayment(request, customerId, organizationId);
}
@Test(expected = NGOPBaseException.class)
public void testInvalidAmount() {
// Given
PaymentRequest request = createValidPaymentRequest();
request.setAmount(BigDecimal.ZERO); // Invalid amount
// When
PaymentValidationHandler.validatePayment(request, 123, 1);
// Then - exception should be thrown
}
@Test(expected = NGOPBaseException.class)
public void testUnsupportedCurrency() {
// Given
PaymentRequest request = createValidPaymentRequest();
request.setFromCurrency("XXX"); // Unsupported currency
// When
PaymentValidationHandler.validatePayment(request, 123, 1);
// Then - exception should be thrown
}
@Test
public void testLargeAmountRequiresEnhancedKYC() {
// Given
PaymentRequest request = createValidPaymentRequest();
request.setAmount(new BigDecimal("15000")); // Large amount
Customer customer = new Customer();
customer.setKycStatus("APPROVED"); // Not enhanced
when(customerDao.getCustomerById(123)).thenReturn(customer);
// When & Then
try {
PaymentValidationHandler.validatePayment(request, 123, 1);
fail("Should have thrown exception for large amount without enhanced KYC");
} catch (NGOPBaseException e) {
assertThat(e.getMessage(), containsString("Enhanced KYC required"));
}
}
@Test
public void testOrganizationSpecificValidation() {
// Given
PaymentRequest request = createValidPaymentRequest();
when(organizationDao.getOrganizationCode(1)).thenReturn("TORFX");
// When & Then - TorFX has different validation rules
PaymentValidationHandler.validatePayment(request, 123, 1);
}
private PaymentRequest createValidPaymentRequest() {
PaymentRequest request = new PaymentRequest();
request.setAmount(new BigDecimal("1000.00"));
request.setFromCurrency("GBP");
request.setToCurrency("EUR");
request.setPaymentMethod("BANK_TRANSFER");
Beneficiary beneficiary = new Beneficiary();
beneficiary.setName("Test Beneficiary");
beneficiary.setAccountNumber("12345678");
beneficiary.setSortCode("123456");
request.setBeneficiary(beneficiary);
return request;
}
}
Currency Calculation Testing
public class FXCalculationTest {
@Test
public void testExchangeRateCalculation() {
// Given
BigDecimal amount = new BigDecimal("1000.00");
BigDecimal exchangeRate = new BigDecimal("1.1234");
// When
BigDecimal convertedAmount = amount.multiply(exchangeRate);
// Then
assertThat(convertedAmount, is(new BigDecimal("1123.4000")));
}
@Test
public void testCurrencyPrecision() {
// Given - Real scenario from production bug
BigDecimal amount = new BigDecimal("999.99");
BigDecimal exchangeRate = new BigDecimal("0.876543");
// When
BigDecimal convertedAmount = amount.multiply(exchangeRate)
.setScale(4, RoundingMode.HALF_UP);
// Then
assertThat(convertedAmount, is(new BigDecimal("876.5387")));
assertThat(convertedAmount.scale(), is(4));
}
@Test
public void testMarginApplication() {
// Given
BigDecimal baseRate = new BigDecimal("1.1234");
BigDecimal marginPercent = new BigDecimal("0.02"); // 2% margin
// When
BigDecimal marginAmount = baseRate.multiply(marginPercent);
BigDecimal customerRate = baseRate.add(marginAmount)
.setScale(6, RoundingMode.HALF_UP);
// Then
assertThat(customerRate, is(new BigDecimal("1.145868")));
}
@Test
public void testZeroAmountHandling() {
// Given
BigDecimal amount = BigDecimal.ZERO;
BigDecimal exchangeRate = new BigDecimal("1.1234");
// When
BigDecimal convertedAmount = amount.multiply(exchangeRate);
// Then
assertThat(convertedAmount, is(BigDecimal.ZERO));
}
}
Integration Testing
Integration tests are where Dione's testing strategy shines. We test complete user journeys and external service integrations.
Service Integration Tests
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:test-application-context.xml"})
@Transactional
public class PaymentServiceIntegrationTest {
@Autowired
private PaymentManagementIMPL paymentService;
@Autowired
private NGOPCusDao customerDao;
@Autowired
private TestDataBuilder testDataBuilder;
@Mock
private PricingEngineService pricingEngineService;
@Test
public void testCompletePaymentFlow() {
// Given - Setup test customer and session
Customer customer = testDataBuilder.createTestCustomer("test@example.com", 1);
PojoMap sessionData = testDataBuilder.createSessionData(customer);
String sessionId = "test-session-123";
// Mock external service
FxRate mockRate = new FxRate();
mockRate.setRate(new BigDecimal("1.1234"));
when(pricingEngineService.getExchangeRate(any(), any(), any(), any()))
.thenReturn(mockRate);
// Create payment request
PaymentRequest request = new PaymentRequest();
request.setAmount(new BigDecimal("1000.00"));
request.setFromCurrency("GBP");
request.setToCurrency("EUR");
request.setPaymentMethod("BANK_TRANSFER");
Beneficiary beneficiary = new Beneficiary();
beneficiary.setName("Test Beneficiary");
beneficiary.setAccountNumber("12345678");
beneficiary.setSortCode("123456");
request.setBeneficiary(beneficiary);
Holder<ResponseStatusHeader> statusHeader = new Holder<>();
// When
PaymentResponse response = paymentService.initiatePayment(
statusHeader, request, sessionId);
// Then
assertThat(response.getStatus(), is("PENDING"));
assertThat(response.getPaymentId(), is(notNullValue()));
assertThat(response.getReference(), is(notNullValue()));
assertThat(response.getExchangeRate(), is(new BigDecimal("1.1234")));
// Verify database state
Payment savedPayment = paymentDao.findById(response.getPaymentId());
assertThat(savedPayment.getCustomerId(), is(customer.getCustomerId()));
assertThat(savedPayment.getOrganizationId(), is(customer.getOrganizationId()));
assertThat(savedPayment.getStatus(), is("PENDING"));
}
@Test
public void testPaymentValidationFailure() {
// Given
Customer customer = testDataBuilder.createTestCustomer("test@example.com", 1);
String sessionId = testDataBuilder.createSessionForCustomer(customer);
PaymentRequest request = new PaymentRequest();
request.setAmount(BigDecimal.ZERO); // Invalid amount
Holder<ResponseStatusHeader> statusHeader = new Holder<>();
// When & Then
try {
paymentService.initiatePayment(statusHeader, request, sessionId);
fail("Should have thrown validation exception");
} catch (NGOPBaseException e) {
assertThat(e.getMessage(), containsString("Amount must be greater than zero"));
}
}
@Test
public void testExternalServiceFailureHandling() {
// Given
Customer customer = testDataBuilder.createTestCustomer("test@example.com", 1);
String sessionId = testDataBuilder.createSessionForCustomer(customer);
PaymentRequest request = testDataBuilder.createValidPaymentRequest();
// Mock external service failure
when(pricingEngineService.getExchangeRate(any(), any(), any(), any()))
.thenThrow(new ExternalServiceException("Pricing engine unavailable"));
// Mock fallback rate
when(fallbackRateService.getLastKnownRate(any(), any(), any()))
.thenReturn(createFallbackRate());
Holder<ResponseStatusHeader> statusHeader = new Holder<>();
// When
PaymentResponse response = paymentService.initiatePayment(
statusHeader, request, sessionId);
// Then - Should succeed with fallback rate
assertThat(response.getStatus(), is("PENDING"));
assertThat(response.getExchangeRate(), is(new BigDecimal("1.0000"))); // Fallback rate
}
}
Database Integration Tests
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:test-application-context.xml"})
@Transactional
public class CustomerDaoIntegrationTest {
@Autowired
private NGOPCusDaoImpl customerDao;
@Test
public void testMultiTenantIsolation() {
// Given - Create customers in different organizations
Customer org1Customer = new Customer();
org1Customer.setEmail("test@example.com");
org1Customer.setOrganizationId(1);
org1Customer.setFirstName("John");
org1Customer.setLastName("Doe");
org1Customer.setPassword("hashedpassword");
customerDao.save(org1Customer);
Customer org2Customer = new Customer();
org2Customer.setEmail("test@example.com"); // Same email, different org
org2Customer.setOrganizationId(2);
org2Customer.setFirstName("Jane");
org2Customer.setLastName("Smith");
org2Customer.setPassword("hashedpassword");
customerDao.save(org2Customer);
// When
Customer foundOrg1 = customerDao.getCustomerByEmail("test@example.com", 1);
Customer foundOrg2 = customerDao.getCustomerByEmail("test@example.com", 2);
// Then
assertThat(foundOrg1, is(notNullValue()));
assertThat(foundOrg2, is(notNullValue()));
assertThat(foundOrg1.getFirstName(), is("John"));
assertThat(foundOrg2.getFirstName(), is("Jane"));
assertThat(foundOrg1.getOrganizationId(), is(1));
assertThat(foundOrg2.getOrganizationId(), is(2));
}
@Test
public void testCustomerKycStatusUpdate() {
// Given
Customer customer = testDataBuilder.createTestCustomer("kyc@example.com", 1);
assertThat(customer.getKycStatus(), is("PENDING"));
assertThat(customer.isActive(), is(false));
// When
Customer updated = customerDao.updateCustomerKycStatus(
customer.getCustomerId(), "APPROVED");
// Then
assertThat(updated.getKycStatus(), is("APPROVED"));
assertThat(updated.isActive(), is(true));
// Verify persistence
Customer reloaded = customerDao.findById(customer.getCustomerId());
assertThat(reloaded.getKycStatus(), is("APPROVED"));
assertThat(reloaded.isActive(), is(true));
}
}
Manual Testing Procedures
Manual testing is a crucial part of our testing strategy, especially for end-to-end user journeys and external system integrations.
Smoke Testing Checklist
PRE-DEPLOYMENT SMOKE TESTS:
1. Authentication Flow
☐ Login with valid credentials (CD org)
☐ Login with valid credentials (TorFX org)
☐ Login failure with invalid credentials
☐ Session timeout after 30 minutes
☐ Logout functionality
2. Customer Journey (New Customer)
☐ Registration form submission
☐ Email verification link
☐ Account activation
☐ First login success
☐ Profile completion
3. Payment Flow (Critical Path)
☐ Get exchange rate quote (GBP → EUR)
☐ Create payment (amount < £1000)
☐ Payment validation success
☐ Payment submission
☐ Payment status tracking
4. External System Integration
☐ Pricing Engine rate retrieval
☐ Salesforce lead creation
☐ Email service (registration email)
☐ SMS service (payment notification)
5. Admin Portal
☐ Admin login
☐ Customer search
☐ Payment status update
☐ KYC document review
PRODUCTION MONITORING:
☐ Application server health check
☐ Database connection pool status
☐ External service response times
☐ Error rate monitoring
User Acceptance Testing
SCENARIO 1: New Customer Onboarding
Test Data:
- Email: uat.customer@example.com
- Organization: Currencies Direct
- Amount: £500 GBP → EUR
Steps:
1. Navigate to customer portal
2. Click "Register New Account"
3. Fill registration form with test data
4. Submit and verify email sent
5. Click verification link in email
6. Complete profile setup
7. Upload KYC documents
8. Wait for approval (manual step)
9. Login and verify dashboard access
Expected Result: Customer can access dashboard and initiate payments
SCENARIO 2: Large Payment Flow
Test Data:
- Existing customer: test.customer@cd.com
- Amount: £15,000 GBP → USD
- Enhanced KYC required
Steps:
1. Login as existing customer
2. Navigate to "Send Money"
3. Enter large amount (£15,000)
4. Select GBP → USD
5. Get rate quote
6. Enter beneficiary details
7. Submit payment
8. Verify enhanced KYC prompt
9. Admin approves enhanced KYC
10. Retry payment submission
Expected Result: Payment processes successfully after enhanced KYC
SCENARIO 3: External Service Failure
Test Data:
- Pricing Engine: Simulate downtime
- Fallback rates: Enabled
Steps:
1. Disable pricing engine (test environment)
2. Login as customer
3. Request exchange rate
4. Verify fallback rate displayed
5. Submit payment with fallback rate
6. Verify payment marked as "pending review"
7. Re-enable pricing engine
8. Admin processes pending payments
Expected Result: System gracefully handles external service failure
Testing Challenges
Real-world testing challenges and how we address them in the Dione platform.
Common Testing Obstacles
🚫 External Service Dependencies
Pricing Engine, Salesforce, and payment processors are unreliable in test environments
🚫 Multi-Tenant Data Complexity
Test data must be isolated by organization, making setup complex
🚫 Financial Calculations
Currency precision and rounding edge cases are hard to test comprehensively
🚫 Legacy Code Coverage
Adding tests to existing code without breaking functionality
Testing Workarounds
// Solution: Smart mocking with realistic responses
@Configuration
@Profile("test")
public class TestExternalServicesConfig {
@Bean
@Primary
public PricingEngineService mockPricingEngineService() {
PricingEngineService mock = Mockito.mock(PricingEngineService.class);
// Setup realistic exchange rates
when(mock.getExchangeRate(eq("GBP"), eq("EUR"), any(), any()))
.thenReturn(createFxRate("GBP", "EUR", "1.1234"));
when(mock.getExchangeRate(eq("GBP"), eq("USD"), any(), any()))
.thenReturn(createFxRate("GBP", "USD", "1.2856"));
// Simulate service timeouts 5% of the time
when(mock.getExchangeRate(eq("GBP"), eq("JPY"), any(), any()))
.thenAnswer(invocation -> {
if (Math.random() < 0.05) {
throw new ExternalServiceException("Timeout");
}
return createFxRate("GBP", "JPY", "157.23");
});
return mock;
}
@Bean
@Primary
public SalesforceService mockSalesforceService() {
SalesforceService mock = Mockito.mock(SalesforceService.class);
// Most operations succeed
when(mock.createLead(any())).thenReturn("SF_LEAD_123");
// Occasional failures for testing error handling
when(mock.updateCustomerStatus(any(), eq("PROBLEM_STATUS")))
.thenThrow(new ExternalServiceException("Salesforce error"));
return mock;
}
}
Test Data Management
@Component
public class TestDataBuilder {
@Autowired
private NGOPCusDao customerDao;
@Autowired
private CacheDataConsumer cacheDataConsumer;
private final AtomicInteger customerCounter = new AtomicInteger(1);
public Customer createTestCustomer(String email, Integer organizationId) {
Customer customer = new Customer();
customer.setEmail(email);
customer.setOrganizationId(organizationId);
customer.setFirstName("Test");
customer.setLastName("Customer" + customerCounter.getAndIncrement());
customer.setPassword("$2a$10$hashOfPassword"); // Hashed password
customer.setActive(true);
customer.setKycStatus("APPROVED");
customer.setCreatedDate(new Date());
return customerDao.save(customer);
}
public String createSessionForCustomer(Customer customer) {
String sessionId = "test-session-" + UUID.randomUUID().toString();
PojoMap sessionData = new PojoMap();
sessionData.put("sessionID", sessionId);
sessionData.put("customerID", customer.getCustomerId());
sessionData.put("organizationId", customer.getOrganizationId());
sessionData.put("customer", customer);
sessionData.put("loginTime", new Date());
sessionData.put("source", "TEST");
// Store in cache with long expiry for testing
cacheDataConsumer.put(sessionId, sessionData, 3600);
return sessionId;
}
public PaymentRequest createValidPaymentRequest() {
PaymentRequest request = new PaymentRequest();
request.setAmount(new BigDecimal("1000.00"));
request.setFromCurrency("GBP");
request.setToCurrency("EUR");
request.setPaymentMethod("BANK_TRANSFER");
Beneficiary beneficiary = new Beneficiary();
beneficiary.setName("Test Beneficiary");
beneficiary.setAccountNumber("12345678");
beneficiary.setSortCode("123456");
beneficiary.setAddress("123 Test Street, Test City");
request.setBeneficiary(beneficiary);
return request;
}
public void cleanupTestData() {
// Clean up test customers (be careful with this!)
List<Customer> testCustomers = customerDao.findByEmailLike("test%");
for (Customer customer : testCustomers) {
customerDao.delete(customer);
}
}
}
Practical Testing Strategy
How to approach testing in a legacy financial services platform without breaking everything.
Adding Tests to Legacy Code
// STEP 1: Characterization Tests
// Before changing legacy code, write tests that capture current behavior
@Test
public void testExistingPaymentValidationBehavior() {
// Document what the system currently does, not what it should do
PaymentRequest request = new PaymentRequest();
request.setAmount(new BigDecimal("-100")); // Negative amount
try {
PaymentValidationHandler.validatePayment(request, 123, 1);
// If this passes, document that negative amounts are currently allowed
// (even if they shouldn't be)
} catch (Exception e) {
// Document the exact exception behavior
assertThat(e.getClass(), is(NGOPBaseException.class));
assertThat(e.getMessage(), containsString("Amount must be greater than zero"));
}
}
// STEP 2: Refactor Safely
// Extract testable units from large methods
public class PaymentValidationHandler {
// Original method (don't change yet)
public static void validatePayment(PaymentRequest request,
Integer customerId, Integer organizationId) {
validateAmount(request.getAmount());
validateCurrencies(request, organizationId);
validateBeneficiary(request.getBeneficiary());
validateComplianceRules(request, customerId, organizationId);
}
// Extracted methods (now testable in isolation)
static void validateAmount(BigDecimal amount) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new NGOPBaseException("Amount must be greater than zero");
}
}
static void validateCurrencies(PaymentRequest request, Integer organizationId) {
// Currency validation logic...
}
}
// STEP 3: Test Extracted Methods
@Test
public void testAmountValidation() {
// Now we can test this logic in isolation
assertThrows(NGOPBaseException.class, () ->
PaymentValidationHandler.validateAmount(BigDecimal.ZERO));
assertThrows(NGOPBaseException.class, () ->
PaymentValidationHandler.validateAmount(new BigDecimal("-100")));
// This should not throw
PaymentValidationHandler.validateAmount(new BigDecimal("100.00"));
}
Testing Best Practices
DO:
✅ Test financial calculations thoroughly
✅ Test multi-tenant data isolation
✅ Test external service failure scenarios
✅ Use realistic test data
✅ Test the happy path AND error cases
✅ Mock external services consistently
✅ Clean up test data after tests
✅ Use descriptive test names
✅ Test one thing per test method
DON'T:
❌ Test UI styling details
❌ Test framework behavior (e.g., Spring injection)
❌ Test getters and setters
❌ Use production API keys in tests
❌ Leave test data in the database
❌ Mock everything (test real business logic)
❌ Write tests that depend on external services
❌ Ignore intermittent test failures
❌ Test private methods directly
PRIORITIZE:
🏆 Payment processing flows
🏆 Authentication and authorization
🏆 Currency conversion accuracy
🏆 Data validation rules
🏆 External service integration points
🏆 Error handling and fallback mechanisms
Running Tests
# Run all tests
mvn test
# Run only unit tests (fast)
mvn test -Dtest=*Test
# Run only integration tests (slower)
mvn test -Dtest=*IntegrationTest
# Run tests for specific component
mvn test -Dtest=Payment*Test
# Run tests with coverage report
mvn test jacoco:report
# Skip tests (for emergency deployments only)
mvn install -DskipTests
# Run tests with specific profile
mvn test -Ptest-environment
🎯 Testing Reality Summary
- Focus on Value: Test what matters for business continuity
- Integration First: Most bugs occur at system boundaries
- Mock Wisely: Mock external services, test business logic
- Data Isolation: Multi-tenant testing requires careful setup
- Manual Testing: Still essential for complex user journeys
- Safety First: Characterization tests before refactoring legacy code