Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Financial Data Integrity Testing: How QA Ensures Accurate Transactions and Balances
#1
   

Financial Data Integrity Testing: How QA Ensures Accurate Transactions and Balances
How do you know a customer's balance is right?

Not "it looks right." Not "the screen matches the API." Right, in the sense that if an auditor, a regulator or the customer's lawyer asked you to prove it, you could.

I ask this in scoping calls and the first answers are usually about UI checks or test accounts. Those matter, but they test whether the app displays what the database says. They say nothing about whether the database is correct. And in financial systems, a wrong number rarely announces itself. It sits quietly in a table, gets copied into a warehouse, flows into a statement, and surfaces months later as a reconciliation break nobody can trace.

So I've stopped thinking about data integrity testing as test cases and started thinking about it as invariants: statements that must be true at all times, no matter what happened. If you can write them down, you can check them. If you can check them, you can run them forever.

Why Example-Based Testing Isn't Enough Here
A normal test says: "Given this input, expect this output." That's useful, and you'll still write plenty. But money systems have an enormous number of paths: retries, reversals, partial refunds, backdated adjustments, currency conversions, batch corrections, manual fixes by operations staff at 2 a.m. You'll never enumerate them all.

Invariants flip the question. Instead of asking "did this scenario work," they ask "after anything and everything that happened, is the data still consistent?" That's why they catch defects nobody predicted.
   

The Invariants I Start With
Here are the ones I'd write on day one for most ledger-based products. Rename the tables to match yours.

1. Every transaction balances. In double-entry systems, debits equal credits within each transaction.

SELECT transaction_id
FROM postings
GROUP BY transaction_id
HAVING SUM(CASE WHEN side = 'DEBIT'  THEN amount ELSE 0 END)
    <> SUM(CASE WHEN side = 'CREDIT' THEN amount ELSE 0 END);
-- Expected: zero rows


2. A balance equals the sum of its history. If you store a running balance for speed, it must match the postings it summarizes.

SELECT a.account_id
FROM accounts a
JOIN (SELECT account_id, SUM(signed_amount) AS total
      FROM postings GROUP BY account_id) p
  ON p.account_id = a.account_id
WHERE a.balance <> p.total;
-- Expected: zero rows


3. Nothing exists without its parent. No posting without a transaction, no transaction without an account, no refund without an original payment.

4. Nothing happens twice. A given idempotency key or external reference maps to exactly one effect.

SELECT external_ref, COUNT(*)
FROM transactions
GROUP BY external_ref
HAVING COUNT(*) > 1;


5. Forbidden states stay forbidden. An account that must not go negative never does. A closed account has no new postings. A refund never exceeds the amount originally captured.

6. Totals tie across systems. What your ledger says was settled agrees with what the processor and the bank report, within the rules you've defined for timing.

7. History is append-only. Posted entries are never edited. Corrections are new entries that reference the old ones, and the audit trail shows who made them and why.

Notice that none of these mention a feature. That's the point. They hold whether the entry came from a mobile app, a batch file, a support tool or a migration script.

Where Defects Actually Come From
When I trace broken invariants back to their cause, a handful of culprits show up repeatedly.

Rounding and allocation. Split 100.00 across three accounts and you get 33.33 three times, with a cent left over. Somebody has to decide where that cent goes, and every service must decide the same way. Test the leftover explicitly, in every currency, including those with no minor unit.

Partial failure. A transfer debits one account and the process dies before crediting the other. Database transactions protect you inside a single database. Across services, you're relying on compensating actions, and those need testing as hard as the main path. Kill the process between steps and check the invariants afterward.

Concurrency. Two requests read the same balance, both decide there's enough money, and both succeed. Run parallel operations against a single account and verify the total.

Time. Value date versus posting date. A cut-off in one time zone and a report in another. Daylight saving shifts. A backdated adjustment that lands inside a period that's already been closed and reported.

Manual intervention. Operations teams fix things by hand, and those fixes bypass the logic you've carefully tested. Check that admin tools write balanced, audited, reversible entries, and that nobody can approve their own change.

Data movement. Every copy of financial data is a new chance to lose or distort it: replication to read replicas, extraction into a warehouse, generation of statements, export to a regulator. This is the least-tested part of most systems.

Testing Data in Motion
For anything that moves or transforms data, I like a layered check, from cheap to thorough.

For migrations, add a before-and-after check on balances for every account, and keep the old system's numbers available until reconciliation is signed off. The one thing I'd never accept is "the counts match, so it's fine."

Making It Continuous
Running these checks once before launch is a start. The real value comes from running them constantly.

Put the cheap invariants into the delivery pipeline against test data after every change. Run the full set on a schedule against production-shaped data, using a read replica or a snapshot so you don't slow down customers. Alert on any violation, and treat a violation as an incident, not a ticket in a backlog. Keep the history of results, because a check that started failing last Tuesday points straight at what changed last Tuesday.

A few practical cautions, since this has costs.
  • Heavy queries on huge tables can hurt. Use incremental checks (only today's postings), partitions, or snapshots rather than full scans on the live system.
  • Replication lag causes false alarms. A mismatch between primary and replica may just be timing. Build in a tolerance window and then escalate if it persists.
  • Not every mismatch is a defect. Timing differences between your ledger and a bank are expected for a while. Define what "expected and aging" looks like, and alert when something ages beyond it.
  • Don't test with data you can't use. Production data should stay inside approved boundaries. Use masked or synthetic data in lower environments, and prove the masking preserves relationships, or your integrity checks will fail for the wrong reasons.

Where AI Helps, and Where It Shouldn't
Machine learning is genuinely useful for spotting unusual patterns in transaction streams, flagging reconciliation breaks that resemble past ones, and suggesting likely causes by clustering similar failures. It can also generate awkward test data, such as odd amounts, mixed currencies and boundary dates.

What it shouldn't do is serve as the judge of correctness. Whether debits equal credits isn't a probabilistic question. Keep the invariants explicit, deterministic and reviewable, and let AI sit around them as a helper that tells humans where to look.

Mistakes I'd Warn You About
Testing the display instead of the data. A beautiful, accurate-looking screen on top of an inconsistent ledger is the most expensive kind of green.

Checking totals only. Offsetting errors cancel out. Check at the level of accounts and transactions too.

No owner for the invariants. They go stale as the schema evolves. Someone has to keep them current, the way someone keeps unit tests current.

Fixing the record without fixing the cause. Correct the bad row, and the same bug writes the next one.

Skipping the boring edge currencies. Zero-decimal and three-decimal currencies break assumptions that work perfectly for dollars and euros.

A Closing Thought
If you do one thing after reading this, write down five statements about your data that must always be true, then write the query that would prove each one false. Run them tonight against a copy of production-shaped data. Either you'll find nothing, and you'll have bought real confidence, or you'll find something, and you'll have found it before an auditor did.

Over time I expect the line between testing and operations to blur here. The invariants you write for QA are exactly the controls finance and risk teams want running every day, and the teams that treat them as shared infrastructure will spend far less time explaining numbers after the fact. If you'd like help defining and automating them, our FinTech Software Testing Services team works with engineering and finance teams on exactly this.
Reply




Users browsing this thread: 1 Guest(s)