Testing Strategies for Financial Systems: Beyond Unit Tests
Comprehensive testing approaches for mission-critical financial applications where bugs cost millions
In financial systems, bugs aren't just annoying—they're catastrophic. A single defect can result in:
After a decade of building trading platforms, we've developed a testing strategy that goes far beyond traditional unit tests.
Traditional testing pyramid doesn't apply to financial systems:
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 |___________|
14Why different?
Instead of testing specific inputs, test mathematical properties.
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});
8Problem: Only tests one scenario. What about edge cases?
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});
53Result: Tests hundreds of random scenarios, catches edge cases like:
Replay historical market data to verify system behavior.
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}
451describe('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});
19Benefits:
Deliberately inject failures to test resilience.
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}
55Generate random inputs to find crashes and edge cases.
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});
20Discovered bugs:
Test your tests by intentionally breaking code.
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}
12If tests still pass, they're insufficient!
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)
9Action: Add tests for surviving mutants.
Compare implementations to find discrepancies.
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});
17Use cases:
Automated regulatory compliance checks.
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});
45Ensure systems meet latency requirements under load.
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});
43Verify API contracts between services.
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});
45Continuous testing in production.
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);
29Traditional code coverage isn't enough:
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?
11Better metrics:
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
9Financial systems require a fundamentally different approach to testing:
Traditional unit tests are necessary but not sufficient. The cost of bugs in financial systems demands comprehensive testing strategies.
We help clients implement:
Contact us to discuss your testing needs.
Technical Writer
NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.
Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.