<\!DOCTYPE html> Testing Reality - Dione Developer Course

🎯 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

Testing Priority Matrix
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

pom.xml - Testing Dependencies
<!-- 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

src/test/resources/test-application-context.xml
<?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

PaymentValidationHandlerTest.java
@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

FXCalculationTest.java
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

PaymentServiceIntegrationTest.java
@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

CustomerDaoIntegrationTest.java
@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

Daily Smoke Test Procedures
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

UAT Test Scenarios
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

External Service Mocking Strategy
// 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

TestDataBuilder.java
@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

Safe Testing Approach
// 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

Dione Testing Guidelines
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

Maven Test Execution
# 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