NV
NordVarg
ServicesTechnologiesIndustriesCase StudiesBlogAboutContact
Get Started

Footer

NV
NordVarg

Software Development & Consulting

GitHubLinkedInTwitter

Services

  • Product Development
  • Quantitative Finance
  • Financial Systems
  • ML & AI

Technologies

  • C++
  • Python
  • Rust
  • OCaml
  • TypeScript
  • React

Company

  • About
  • Case Studies
  • Blog
  • Contact

© 2025 NordVarg. All rights reserved.

October 20, 2024
•
NordVarg Team
•

Testing Strategies for Financial Systems: Beyond Unit Tests

Comprehensive testing approaches for mission-critical financial applications where bugs cost millions

TestingTestingQAFinancial SystemsRisk Management
9 min read
Share:

Introduction#

In financial systems, bugs aren't just annoying—they're catastrophic. A single defect can result in:

  • Millions in trading losses
  • Regulatory fines
  • Reputational damage
  • License revocation

After a decade of building trading platforms, we've developed a testing strategy that goes far beyond traditional unit tests.

The Testing Pyramid for Finance#

Traditional testing pyramid doesn't apply to financial systems:

plaintext
1        Traditional               Financial Systems
2        
3        /\                        ___________
4       /  \  E2E                 |           | Property
5      /____\                     |  Property | Tests
6     /      \  Integration       |___________|
7    /________\                   |           | Simulation
8   /          \ Unit             | Simulation| Tests
9  /____________\                 |___________|
10                                 |           | Integration
11                                 |___________|
12                                 |   Unit    |
13                                 |___________|
14

Why different?

  • Unit tests can't catch systemic risks
  • Integration tests miss edge cases in market conditions
  • E2E tests don't cover financial scenarios

1. Property-Based Testing#

Instead of testing specific inputs, test mathematical properties.

Traditional Unit Test#

typescript
1describe('calculatePnL', () => {
2  it('calculates profit correctly', () => {
3    const position = { symbol: 'AAPL', quantity: 100, avgPrice: 150 };
4    const currentPrice = 160;
5    expect(calculatePnL(position, currentPrice)).toBe(1000);
6  });
7});
8

Problem: Only tests one scenario. What about edge cases?

Property-Based Test#

typescript
1import fc from 'fast-check';
2
3describe('calculatePnL properties', () => {
4  it('PnL should be zero when current price equals avg price', () => {
5    fc.assert(
6      fc.property(
7        fc.float({ min: 0.01, max: 10000 }),  // avgPrice
8        fc.integer({ min: 1, max: 1000000 }), // quantity
9        (avgPrice, quantity) => {
10          const position = { symbol: 'AAPL', quantity, avgPrice };
11          const pnl = calculatePnL(position, avgPrice);
12          expect(Math.abs(pnl)).toBeLessThan(0.001);
13        }
14      )
15    );
16  });
17
18  it('PnL should be proportional to quantity', () => {
19    fc.assert(
20      fc.property(
21        fc.float({ min: 0.01, max: 10000 }),
22        fc.float({ min: 0.01, max: 10000 }),
23        fc.integer({ min: 1, max: 1000 }),
24        (avgPrice, currentPrice, quantity) => {
25          const pos1 = { symbol: 'AAPL', quantity, avgPrice };
26          const pos2 = { symbol: 'AAPL', quantity: quantity * 2, avgPrice };
27          
28          const pnl1 = calculatePnL(pos1, currentPrice);
29          const pnl2 = calculatePnL(pos2, currentPrice);
30          
31          expect(Math.abs(pnl2 - pnl1 * 2)).toBeLessThan(0.01);
32        }
33      )
34    );
35  });
36
37  it('buying and selling should cancel out', () => {
38    fc.assert(
39      fc.property(
40        fc.float({ min: 0.01, max: 10000 }),
41        fc.integer({ min: 1, max: 1000 }),
42        (price, quantity) => {
43          const buy = { symbol: 'AAPL', quantity, avgPrice: price };
44          const sell = { symbol: 'AAPL', quantity: -quantity, avgPrice: price };
45          
46          const totalPnL = calculatePnL(buy, price) + calculatePnL(sell, price);
47          expect(Math.abs(totalPnL)).toBeLessThan(0.001);
48        }
49      )
50    );
51  });
52});
53

Result: Tests hundreds of random scenarios, catches edge cases like:

  • Negative prices
  • Zero quantities
  • Extreme values
  • Rounding errors

2. Simulation Testing#

Replay historical market data to verify system behavior.

Market Replay Framework#

typescript
1interface MarketEvent {
2  timestamp: number;
3  type: 'QUOTE' | 'TRADE' | 'ORDER_BOOK';
4  data: any;
5}
6
7class MarketSimulator {
8  private events: MarketEvent[] = [];
9  
10  async loadHistoricalData(date: string, symbol: string) {
11    // Load from database or files
12    this.events = await loadMarketData(date, symbol);
13  }
14  
15  async replay(strategy: TradingStrategy) {
16    const results = [];
17    
18    for (const event of this.events) {
19      const orders = await strategy.onMarketData(event);
20      
21      for (const order of orders) {
22        const execution = this.simulateExecution(order, event);
23        results.push(execution);
24      }
25    }
26    
27    return this.analyzeResults(results);
28  }
29  
30  private simulateExecution(order: Order, marketState: MarketEvent) {
31    // Realistic execution simulation
32    const slippage = this.calculateSlippage(order.quantity, marketState);
33    const executionPrice = order.limitPrice 
34      ? order.limitPrice 
35      : marketState.data.price + slippage;
36      
37    return {
38      orderId: order.id,
39      executedPrice: executionPrice,
40      executedQuantity: order.quantity,
41      timestamp: marketState.timestamp
42    };
43  }
44}
45

Usage#

typescript
1describe('TradingStrategy - Flash Crash 2010', () => {
2  it('should not blow up during flash crash', async () => {
3    const simulator = new MarketSimulator();
4    await simulator.loadHistoricalData('2010-05-06', 'SPY');
5    
6    const strategy = new MomentumStrategy({
7      maxPositionSize: 1000,
8      stopLoss: 0.02
9    });
10    
11    const results = await simulator.replay(strategy);
12    
13    // Verify risk controls worked
14    expect(results.maxDrawdown).toBeLessThan(0.05);
15    expect(results.maxPositionSize).toBeLessThanOrEqual(1000);
16    expect(results.totalLoss).toBeLessThan(50000);
17  });
18});
19

Benefits:

  • Tests against real market conditions
  • Catches issues with market microstructure
  • Validates risk controls
  • Regression testing for known events

3. Chaos Engineering#

Deliberately inject failures to test resilience.

Infrastructure Failures#

typescript
1class ChaosTest {
2  async testDatabaseFailover() {
3    const tradingSystem = new TradingSystem();
4    await tradingSystem.start();
5    
6    // System running normally
7    const order1 = await tradingSystem.placeOrder({
8      symbol: 'AAPL',
9      quantity: 100,
10      type: 'MARKET'
11    });
12    expect(order1.status).toBe('ACCEPTED');
13    
14    // Inject failure: kill primary database
15    await this.killPrimaryDB();
16    
17    // System should failover to replica
18    await sleep(2000); // failover time
19    
20    // Orders should still work
21    const order2 = await tradingSystem.placeOrder({
22      symbol: 'AAPL',
23      quantity: 100,
24      type: 'MARKET'
25    });
26    expect(order2.status).toBe('ACCEPTED');
27    
28    // Verify no data loss
29    const allOrders = await tradingSystem.getOrders();
30    expect(allOrders).toContainEqual(order1);
31    expect(allOrders).toContainEqual(order2);
32  }
33  
34  async testNetworkPartition() {
35    // Simulate network split between order service and execution service
36    await this.blockNetwork('order-service', 'execution-service');
37    
38    // Orders should queue, not fail
39    const order = await tradingSystem.placeOrder({
40      symbol: 'AAPL',
41      quantity: 100
42    });
43    
44    expect(order.status).toBe('PENDING');
45    
46    // Restore network
47    await this.unblockNetwork('order-service', 'execution-service');
48    await sleep(1000);
49    
50    // Order should execute
51    const updatedOrder = await tradingSystem.getOrder(order.id);
52    expect(updatedOrder.status).toBe('FILLED');
53  }
54}
55

4. Fuzzing#

Generate random inputs to find crashes and edge cases.

typescript
1import { Fuzzer } from '@jazzer.js/core';
2
3describe('Order Parser Fuzzing', () => {
4  it('should never crash on malformed input', () => {
5    const fuzzer = new Fuzzer({
6      target: (data: Buffer) => {
7        try {
8          parseOrder(data.toString());
9        } catch (e) {
10          // Catching errors is OK
11          // But crashes/hangs are not
12        }
13      },
14      iterations: 100000
15    });
16    
17    fuzzer.run();
18  });
19});
20

Discovered bugs:

  • Null pointer dereferences
  • Buffer overflows
  • Infinite loops
  • Stack overflows

5. Mutation Testing#

Test your tests by intentionally breaking code.

typescript
1// Original code
2function calculateRisk(position: Position): number {
3  if (position.quantity === 0) return 0;
4  return position.quantity * position.price * position.volatility;
5}
6
7// Mutation: Remove zero check
8function calculateRisk(position: Position): number {
9  // if (position.quantity === 0) return 0;  // MUTATED
10  return position.quantity * position.price * position.volatility;
11}
12

If tests still pass, they're insufficient!

bash
1$ npx stryker run
2
3Mutant 1: SURVIVED - removed zero check
4Mutant 2: KILLED - changed * to +
5Mutant 3: SURVIVED - changed volatility to 1.0
6Mutant 4: KILLED - removed price multiplication
7
8Mutation Score: 50% (2/4 mutants killed)
9

Action: Add tests for surviving mutants.

6. Differential Testing#

Compare implementations to find discrepancies.

typescript
1describe('Risk Engine - Differential Testing', () => {
2  it('new engine matches legacy engine', () => {
3    fc.assert(
4      fc.property(
5        generateRandomPortfolio(),
6        (portfolio) => {
7          const legacyRisk = legacyRiskEngine.calculate(portfolio);
8          const newRisk = newRiskEngine.calculate(portfolio);
9          
10          // Allow small floating point differences
11          expect(Math.abs(legacyRisk - newRisk)).toBeLessThan(0.01);
12        }
13      )
14    );
15  });
16});
17

Use cases:

  • Migrating to new implementation
  • Comparing OCaml vs Rust versions
  • Testing optimization changes

7. Compliance Testing#

Automated regulatory compliance checks.

typescript
1describe('Regulatory Compliance', () => {
2  it('enforces position limits', async () => {
3    const account = new Account({ maxPositionSize: 1000 });
4    
5    // Should accept valid order
6    await expect(
7      account.placeOrder({ symbol: 'AAPL', quantity: 500 })
8    ).resolves.toBeDefined();
9    
10    // Should reject oversized order
11    await expect(
12      account.placeOrder({ symbol: 'AAPL', quantity: 1500 })
13    ).rejects.toThrow('Exceeds position limit');
14  });
15  
16  it('enforces wash sale rules', async () => {
17    const account = new Account();
18    
19    // Sell at loss
20    await account.placeOrder({ symbol: 'AAPL', quantity: -100, price: 140 });
21    // Original cost basis was $150, loss of $1000
22    
23    // Try to repurchase within 30 days (wash sale)
24    await expect(
25      account.placeOrder({ symbol: 'AAPL', quantity: 100, price: 145 })
26    ).rejects.toThrow('Wash sale violation');
27  });
28  
29  it('maintains audit trail', async () => {
30    const order = await account.placeOrder({ 
31      symbol: 'AAPL', 
32      quantity: 100 
33    });
34    
35    const auditLog = await getAuditTrail(order.id);
36    
37    expect(auditLog).toContainEqual({
38      action: 'ORDER_CREATED',
39      timestamp: expect.any(Number),
40      user: expect.any(String),
41      details: expect.any(Object)
42    });
43  });
44});
45

8. Performance Testing#

Ensure systems meet latency requirements under load.

typescript
1describe('Order Processing - Performance', () => {
2  it('processes orders within 1ms p99', async () => {
3    const latencies: number[] = [];
4    
5    for (let i = 0; i < 10000; i++) {
6      const start = process.hrtime.bigint();
7      
8      await orderService.process({
9        symbol: 'AAPL',
10        quantity: 100,
11        type: 'MARKET'
12      });
13      
14      const end = process.hrtime.bigint();
15      latencies.push(Number(end - start) / 1_000_000); // convert to ms
16    }
17    
18    latencies.sort((a, b) => a - b);
19    const p99 = latencies[Math.floor(latencies.length * 0.99)];
20    
21    expect(p99).toBeLessThan(1.0);
22  });
23  
24  it('handles 10k orders/second', async () => {
25    const startTime = Date.now();
26    const promises = [];
27    
28    for (let i = 0; i < 10000; i++) {
29      promises.push(orderService.process({
30        symbol: 'AAPL',
31        quantity: 100
32      }));
33    }
34    
35    await Promise.all(promises);
36    
37    const duration = (Date.now() - startTime) / 1000;
38    const throughput = 10000 / duration;
39    
40    expect(throughput).toBeGreaterThan(10000);
41  });
42});
43

9. Contract Testing#

Verify API contracts between services.

typescript
1import { PactV3 } from '@pact-foundation/pact';
2
3describe('Risk Service Contract', () => {
4  const pact = new PactV3({
5    consumer: 'OrderService',
6    provider: 'RiskService'
7  });
8  
9  it('validates order risk', async () => {
10    await pact
11      .given('account has sufficient margin')
12      .uponReceiving('a risk check request')
13      .withRequest({
14        method: 'POST',
15        path: '/risk/validate',
16        body: {
17          accountId: '12345',
18          symbol: 'AAPL',
19          quantity: 100,
20          price: 150
21        }
22      })
23      .willRespondWith({
24        status: 200,
25        body: {
26          approved: true,
27          requiredMargin: 15000,
28          availableMargin: 50000
29        }
30      });
31    
32    await pact.executeTest(async (mockServer) => {
33      const riskClient = new RiskClient(mockServer.url);
34      const result = await riskClient.validate({
35        accountId: '12345',
36        symbol: 'AAPL',
37        quantity: 100,
38        price: 150
39      });
40      
41      expect(result.approved).toBe(true);
42    });
43  });
44});
45

10. Post-Deployment Verification#

Continuous testing in production.

typescript
1class ProductionMonitor {
2  async runSyntheticTransactions() {
3    // Use test accounts with real system
4    const testOrder = await tradingSystem.placeOrder({
5      accountId: 'TEST_ACCOUNT_001',
6      symbol: 'AAPL',
7      quantity: 1,
8      type: 'MARKET'
9    }, { synthetic: true });
10    
11    if (testOrder.status !== 'FILLED') {
12      await this.alertOncall('Synthetic transaction failed');
13    }
14  }
15  
16  async verifyDataConsistency() {
17    // Compare position in different systems
18    const tradingPosition = await tradingDB.getPosition('AAPL');
19    const reportingPosition = await reportingDB.getPosition('AAPL');
20    
21    if (Math.abs(tradingPosition - reportingPosition) > 0) {
22      await this.alertOncall('Position mismatch detected');
23    }
24  }
25}
26
27// Run every 5 minutes
28setInterval(() => monitor.runSyntheticTransactions(), 5 * 60 * 1000);
29

Test Coverage Metrics#

Traditional code coverage isn't enough:

typescript
1// 100% code coverage, but inadequate testing
2function divide(a: number, b: number): number {
3  return a / b;
4}
5
6// Test that gives 100% coverage
7expect(divide(10, 2)).toBe(5);
8
9// But doesn't catch divide by zero!
10expect(divide(10, 0)).toBe(Infinity); // Should this be allowed?
11

Better metrics:

  • Mutation score: % of mutants killed
  • Property coverage: % of mathematical properties tested
  • Scenario coverage: % of market scenarios tested
  • Edge case coverage: % of boundary conditions tested

Our Testing Stack#

plaintext
1Property-Based: fast-check
2Simulation: Custom market replay engine
3Chaos: Chaos Mesh + custom tools
4Fuzzing: @jazzer.js/core
5Mutation: Stryker
6Contract: Pact
7Performance: k6 + custom benchmarks
8Monitoring: Datadog + custom synthetic transactions
9

Conclusion#

Financial systems require a fundamentally different approach to testing:

  1. Property-based testing for mathematical correctness
  2. Simulation testing for real market conditions
  3. Chaos engineering for resilience
  4. Fuzzing for security and edge cases
  5. Mutation testing to validate test quality
  6. Differential testing for migrations
  7. Compliance testing for regulatory requirements
  8. Performance testing for latency guarantees
  9. Contract testing for service boundaries
  10. Production monitoring for continuous verification

Traditional unit tests are necessary but not sufficient. The cost of bugs in financial systems demands comprehensive testing strategies.

Our Approach#

We help clients implement:

  • Property-based testing frameworks
  • Market simulation infrastructure
  • Chaos engineering practices
  • Comprehensive test strategies

Contact us to discuss your testing needs.

NT

NordVarg Team

Technical Writer

NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.

TestingQAFinancial SystemsRisk Management

Join 1,000+ Engineers

Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.

✓Weekly articles
✓Industry insights
✓No spam, ever

Related Posts

Nov 11, 2025•9 min read
Property-Based Testing in Finance: From Hypothesis to Production
Testingproperty-based-testinghypothesis
Dec 31, 2024•5 min read
Property-Based Testing for Financial Systems
Testingproperty-based-testinghypothesis
Dec 31, 2024•9 min read
Performance Regression Testing in CI/CD
Testingperformanceci-cd

Interested in working together?