Database Testing in Software Testing
Database testing is the practice of verifying that an application's data layer stores, retrieves and protects data correctly. It covers schema objects, CRUD operations, transactions and ACID behaviour, referential integrity, and query performance. It validates the data itself rather than the screens above it, because data usually outlives the code that wrote it.
Most teams test what a user can see and assume the database underneath is fine. It often isn't. A UI test passes because the screen rounded a number for display; the value actually stored is wrong, and the report built on that column six months later is wrong in a way nobody can trace back. Database testing closes that gap by checking the data layer directly, with SQL, against the rules the schema is supposed to enforce.
This guide covers what database testing is, the three types you need, how to test transactions and integrity, what to do about migrations and test data, the tools that matter, and the mistakes that quietly waste the effort. It is scoped to operational databases: the OLTP systems your application reads and writes on every request.
Key takeaways
- Database testing verifies stored data and schema behaviour, not the screens above it.
- It splits into structural, functional and non-functional types — you need all three.
- Constraints are tests. Prove the database rejects bad data instead of trusting the application layer.
- Migrations need their own tests, run forward and backward, against realistic data volumes.
- Shared, dirty test databases are the single biggest cause of flaky data tests.
What is database testing in software testing?
Database testing is the verification of an application's data layer: that data is stored correctly, retrieved correctly, protected by the constraints it should be, and served fast enough. It sits below the interface tests most people picture when they think of QA, and it asserts against the database itself using SQL rather than against rendered output.
The distinction matters because the two layers can disagree. An application can display the right value while persisting the wrong one, silently truncate a string on insert, or write a duplicate that only surfaces when a nightly job chokes on it. None of those show up in a passing UI test. Database testing is the layer of the types of software testing that catches them, and it is frequently the layer teams skip entirely.
Why does database testing matter?
Database testing matters because data outlives the code that wrote it. Applications get rewritten, front ends get replaced, services get split apart — the data survives all of it. A defect in application logic ships a bad release; a defect that corrupts stored data ships a bad release and leaves a permanent artifact in your records that every future system has to cope with.
The practical arguments for investing here:
- Corruption compounds. A bad UI is fixed by a redeploy. Bad rows have to be found, understood and repaired, often without knowing when the corruption started.
- Data tests are stable. SQL assertions do not break when a button moves. They are dramatically less flaky than end-to-end UI tests and cheaper to maintain.
- Failures are silent. Referential integrity breaks, rounding errors and truncated fields do not throw exceptions. Nothing alerts you.
- Downstream blast radius. Reporting, analytics, billing and machine learning all read the same tables. One wrong column feeds many wrong answers.
- Compliance. Auditors ask what your data controls are. "The application checks it" is a weak answer when the database will happily accept anything.
If your test strategy currently stops at the API boundary, adding database assertions is usually the highest return per hour available. It is a core part of any serious quality assurance services engagement, precisely because it finds a class of defect nothing else does.
What are the types of database testing?
Database testing is conventionally grouped into three types: structural, functional and non-functional. They answer different questions and you need all three — structural alone tells you the container is right, functional alone tells you nothing about whether it survives load.
| Type | What it verifies | Typical checks |
|---|---|---|
| Structural | The database objects themselves | Schema and table definitions, column names and data types, indexes, primary and foreign keys, constraints, stored procedures, triggers, views |
| Functional | Behaviour the application depends on | CRUD operations, business rules, transactions and rollback, stored-procedure logic, trigger side effects |
| Non-functional | How the database behaves under real conditions | Query performance, load and concurrency, security and access control, backup and recovery |
Structural testing
Structural testing verifies that the database is built the way the design says. Column types match the domain (a currency amount is not a float), indexes exist on the columns your queries filter on, primary and foreign keys are declared, NOT NULL and CHECK constraints are present, and views resolve. It also covers the programmable objects people forget are code: stored procedures and triggers are logic, they can have bugs, and they need tests like anything else.
The highest-value structural test is often the simplest: assert that the schema in your test environment matches what your migration scripts produce. Environments drift, and a column that exists in staging but not production is a release incident waiting to happen.
Functional testing
Functional testing verifies that operations do what the business expects. Insert a record through the application and confirm every field landed with the right value and the right precision. Update it and check the audit columns moved. Delete it and check the cascade behaved. Run the business rules that live in the database — a trigger that recalculates a balance, a procedure that applies a discount — and assert the result directly rather than inferring it from a screen.
Non-functional testing
Non-functional testing covers everything about how the database behaves rather than what it returns. Query latency under realistic data volumes, behaviour under concurrent writes, deadlock handling, connection-pool exhaustion, index effectiveness, and whether a backup actually restores. Much of this overlaps with performance testing, and the two disciplines should share environments and data: a query that returns in 3ms against a thousand seeded rows can take 30 seconds against the twenty million rows production carries.
One scope note. This article covers operational databases. When the target is an analytical store fed by pipelines, the failure modes change and so do the techniques — that is data warehouse testing, which deals with source-to-target mapping, reconciliation and slowly changing dimensions rather than single transactions.
How do you test transactions and ACID properties?
You test transactions by deliberately failing them and asserting the database left no trace. ACID — atomicity, consistency, isolation, durability — is a set of guarantees, and each one is testable with a specific technique rather than by inspection.
- Atomicity: start a multi-statement transaction, force an error partway through, and assert that none of the statements persisted. A half-completed transfer that debits one account without crediting the other is the classic failure this catches.
- Consistency: attempt operations that would leave the database violating its own constraints and assert they are rejected. The database should refuse to enter an invalid state, not merely be asked politely not to.
- Isolation: run concurrent sessions against the same rows and assert the outcome matches your configured isolation level. Two clients incrementing the same counter must not lose an update; a read must not see another session's uncommitted work.
- Durability: commit, then kill the database process, restart, and confirm the committed data is still there. This is rarely tested and occasionally very revealing.
Isolation is the one worth extra attention. Most concurrency bugs are not visible in single-threaded tests at all — they need two sessions racing on purpose. Write those tests explicitly, because production will run them for you whether you do or not.
How do you test data integrity and referential integrity?
Test data integrity by attacking the database with data it should reject, then querying for the damage bad data would leave behind. The principle is that constraints are tests: a foreign key you declared but never verified is an assumption, not a guarantee.
A working sequence:
- Prove the rejections. Try to insert NULL into a NOT NULL column, a duplicate into a UNIQUE column, an out-of-range value into a CHECK-constrained column, and a child row pointing at a non-existent parent. Each attempt must fail. If any succeeds, the constraint is missing or disabled.
- Prove the cascades. Delete a parent row and assert children behave as designed — cascaded, restricted, or set to null. Getting this wrong either orphans data or deletes far more than intended.
- Hunt for existing damage. Query for orphaned children, duplicates on business keys, values outside their valid domain and mismatched totals between related tables. Run these as standing checks, not once.
Referential integrity deserves specific attention when a system has been running for a while, or when foreign keys were dropped for performance and "enforced in the application" instead. That arrangement holds exactly until one bulk script or one buggy service writes around it, and the orphan rows accumulate quietly from then on.
How do you test a database migration?
Test a migration by running it against a realistic copy of the data, verifying the result against explicit expectations, and then rolling it back to prove you can. Migrations are the highest-risk database change you make: they are usually one-way in practice, they run against production data you have not seen, and they run under time pressure during a release.
What a migration test needs to cover:
- Forward correctness. The resulting schema matches the target definition exactly — columns, types, indexes, constraints and defaults.
- Data preservation. Row counts reconcile before and after. Transformed columns hold the values you expect. Nothing was truncated by a narrowed type.
- Rollback. The down migration runs and returns the schema to its prior state. Untested rollbacks fail at the worst possible moment.
- Idempotency and re-runs. Running the migration twice does not corrupt anything, because sooner or later something will retry it.
- Duration and locking. Time it against production-scale volumes. An
ALTER TABLEthat takes two seconds on ten thousand rows can lock a table for an hour on fifty million.
Tools like Liquibase and Flyway make migrations versioned, ordered and repeatable, which is a prerequisite for testing them at all. They do not verify that the data is correct afterwards — that is still your assertions.
What are data quality checks?
Data quality checks are assertions about the content of the data rather than the structure holding it. Structural tests confirm a column is a DATE; quality checks confirm the dates are not in the year 1900, that 40% of them are not null, and that they are not all identical because an upstream default leaked in.
The standard dimensions to assert against:
- Completeness: required fields populated, expected row volumes present.
- Uniqueness: no duplicates on business keys, even where no UNIQUE constraint exists.
- Validity: values inside their allowed domain, formats and ranges respected.
- Consistency: related tables agree — order totals match the sum of their line items.
- Timeliness: data is as fresh as the process promises.
- Accuracy: values match the real-world fact they represent, which usually requires reconciliation against a source of truth.
Quality checks earn their keep when they run continuously against real data rather than once during a test cycle. Because everything downstream inherits these defects, the same assertions belong wherever the data lands next — a point that anyone building data analytics services on top of an operational database learns the hard way.
What SQL do testers actually need?
Testers need enough SQL to prove a claim about stored data, which is a much smaller set than a developer or DBA needs. Confident SELECT with JOINs, GROUP BY with HAVING, aggregates, set operations and basic plan reading will cover the large majority of real database testing work.
The queries that come up constantly:
-- Reconciliation: counts must match between two tables
SELECT COUNT(*) FROM source_orders;
SELECT COUNT(*) FROM target_orders;
-- Orphans: child rows whose parent no longer exists
SELECT o.order_id
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.customer_id
WHERE c.customer_id IS NULL;
-- Duplicates on a column that is supposed to be unique
SELECT email, COUNT(*)
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;
-- Rows present in source but missing from target
SELECT order_id FROM source_orders
EXCEPT
SELECT order_id FROM target_orders;
-- Consistency: does the stored total match its line items?
SELECT o.order_id, o.total_amount, SUM(l.line_amount) AS calculated
FROM orders o
JOIN order_lines l ON l.order_id = o.order_id
GROUP BY o.order_id, o.total_amount
HAVING o.total_amount <> SUM(l.line_amount);
Add EXPLAIN (or EXPLAIN ANALYZE, depending on your engine) to that list. Reading a
query plan well enough to spot a sequential scan where an index scan should be is the difference between
reporting "this page is slow" and reporting "this query is slow because it isn't using the index on
customer_id".
How do you manage test data and mask PII?
Manage test data by generating it deterministically from code wherever you can, and by masking it irreversibly when you must copy from production. The default many teams fall into — restoring a production dump into a shared test database — is both the biggest source of flaky data tests and a live compliance problem.
The approaches, in rough order of preference:
- Seeded fixtures. Build known data from migration scripts plus a seed step. Fast, deterministic, versioned alongside the code, and every run starts identical.
- Synthetic generation. Generate volume that matches production's shape — cardinality, distribution, skew — without any real records. Essential for performance work, where a uniformly random dataset gives misleadingly good numbers.
- Masked production subsets. When you genuinely need production's messiness, take a referentially consistent subset and mask it. Masking must be irreversible and consistent: the same customer must map to the same fake name across every table, or joins break and the data becomes useless.
Two rules worth holding firm on. Never use unmasked production PII in a test environment — under GDPR and similar regimes, a test database full of real customer data is a production database wearing a disguise, with weaker access controls and more copies. And never share one mutable test database between suites: the moment two runs mutate the same rows, failures become random and the team stops believing the tests. At larger data volumes this becomes an engineering problem in its own right, which is where dedicated big data testing practices apply.
What tools are used for database testing?
Database testing tools fall into four groups: clients for exploration, unit-test frameworks for logic, assertion frameworks for data quality, and migration tools for schema change. Most teams use one from each rather than a single product.
| Tool | Category | What it does |
|---|---|---|
| SQL clients (DBeaver, DataGrip, psql, SSMS) | Exploration | Ad-hoc querying, plan inspection and manual verification. Where investigation starts. |
| tSQLt | Unit testing | Open-source unit-test framework for SQL Server. Runs tests inside the database with transaction-based isolation and lets you fake tables and stored procedures. |
| DbUnit | State management | JUnit extension that puts a database into a known state before each test and compares it against expected datasets afterwards. |
| Great Expectations | Data quality | Open-source Python framework for declaring expectations about data (nullability, uniqueness, ranges, distributions) and validating datasets against them. |
| Liquibase / Flyway | Migrations | Version-control schema changes so migrations are ordered, repeatable and reviewable. Both support rollback paths. |
| Testcontainers | Environments | Spins up a real, disposable database in a container per test run, removing the shared-environment problem entirely. |
Pick for your stack rather than by popularity. A SQL Server shop gets more from tSQLt than from a Python framework; a team already running containers in CI gets more from Testcontainers than from any amount of environment-management tooling.
How do you automate database testing in CI?
Automate database testing by building the database from scratch on every pipeline run, seeding it, asserting against it, and destroying it. The disposable database is the whole trick: it removes shared state, which removes the ordering dependencies and leftover rows that make data tests flaky.
A pipeline that works:
- Start a containerised database at the same major version as production.
- Apply every migration from empty, which tests the migration path on every commit for free.
- Seed deterministic fixtures.
- Run structural assertions, constraint tests, stored-procedure unit tests and data quality checks.
- Wrap each test in a transaction and roll it back, so tests cannot see each other's writes.
- Tear the container down.
Keep the fast checks on every commit and push the slow ones — large-volume performance runs, full restore drills — to a nightly job. The general principles that keep any suite trustworthy apply here too: stability before coverage, independent tests, actionable failures. Our test automation guide covers that framework in depth, and a database suite is simply the layer of it that speaks SQL. Teams standing this up alongside API and UI coverage usually fold it into a broader automation testing practice rather than treating it as a separate initiative.
What are the common database testing pitfalls?
Most database testing failures are environmental rather than technical. The tests are usually reasonable; the conditions they run under are not.
- Testing against a shared, dirty database. Leftover rows from previous runs make results depend on execution order. This causes more abandoned data suites than any other single factor.
- No rollback or cleanup. Tests that commit and never clean up poison the environment for everything after them.
- Testing at toy volumes. A thousand rows hides every performance problem you have. Missing indexes only hurt at scale, and scale is exactly where you find out.
- Ignoring query plans. A query can return correct results and still be a production incident. Correctness and performance are separate assertions.
- Trusting the application layer to enforce rules. If the constraint is not in the database, it will eventually be violated by a script, a migration or a second service.
- Unmasked production data in test. A compliance exposure that also makes tests non-deterministic, since the data changes with every refresh.
- Skipping stored procedures and triggers. They are code. Untested code in a database is worse than untested code in an application, because it runs invisibly.
- Asserting through the UI. If your only check on stored data is what a screen displays, you are testing the formatter, not the database.
How Appsierra helps
Appsierra builds AI-native quality engineering pods that treat the data layer as a first-class test target rather than an afterthought. That means schema and constraint verification, transaction and integrity tests, migration rehearsal against realistic volumes, and data quality assertions that run in your pipeline — built by senior engineers who have debugged production data incidents, not by contractors following a script.
We work as the accountable middle between giant systems integrators and cheap talent marketplaces: ISO 9001 and ISO 27001 certified, CMMI-aligned, senior-led vetted pods, de-risked by our own evaluation platform, and typically productive in about seven days. Engagements start with a risk-free paid pilot tied to a metric you choose, so you can judge the work before committing to it.
If your data layer is untested, or you have inherited a database nobody fully trusts, our quality assurance services team can assess it and stand up a working suite. Where the volumes or pipelines are the hard part, our big data testing practice covers that ground. Book a free 30-minute call and we will give you an honest read on where the risk actually sits.
Frequently asked questions
What is the difference between database testing and ETL testing?
Database testing validates the operational (OLTP) data layer an application writes to: schema objects, CRUD operations, transactions, constraints and query performance. ETL testing validates data moving through a pipeline into an analytical store, focusing on source-to-target mapping, transformation rules, load completeness and reconciliation. The techniques overlap, but the scope, data volumes and failure modes are different.
Do you need SQL to do database testing?
Yes, in practice. Database testing means asserting against stored data directly, and SQL is how you do that. You do not need to be a database administrator, but you need confident SELECT, JOIN, GROUP BY, HAVING, aggregate and set-operation skills, plus enough familiarity with EXPLAIN to tell a slow query from a missing index.
What are the types of database testing?
Database testing is usually grouped into three types. Structural testing verifies the database objects themselves: schema, tables, columns, data types, indexes, constraints, stored procedures, triggers and views. Functional testing verifies behaviour the application depends on: CRUD operations, business rules and transactions. Non-functional testing verifies performance, load, security and recovery characteristics.
How do you test data integrity in a database?
Test that the database enforces its own rules rather than trusting the application to. Attempt to insert rows that violate NOT NULL, UNIQUE, CHECK and foreign-key constraints and assert the database rejects them. Then query for orphaned child rows, duplicates on unique keys, out-of-range values and broken parent-child relationships, and confirm deletes cascade or restrict exactly as designed.
Can database testing be automated?
Yes, and it should be. Schema checks, constraint tests, stored-procedure tests and data quality assertions all run well in a pipeline. Practical setups spin up a disposable database from migration scripts, seed known fixtures, run assertions with a framework such as tSQLt, DbUnit or Great Expectations, then tear the database down so each run starts from an identical state.
Ready to put this into practice?
Appsierra's expert-supervised QA and AI engineering pods help teams ship higher-quality software faster — with senior accountability and a low-risk pilot. Tell us what you're working on.