PRACTICAL GUIDE / how to write SQL test cases
How to Write SQL Test Cases: Examples for QA Teams
How to write SQL test cases with objectives, setup data, validation queries, expected results, evidence, cleanup, and practical QA examples.
In this guide9 sections
- Start with an invariant, not a query
- Use a complete SQL test case template
- Make setup data diagnostic
- Write a validation query that returns failures
- State cardinality and values precisely
- Add negative and non-change assertions
- Design safe setup and cleanup
- Capture evidence and diagnose failures
- Review cases for automation readiness
What you will learn
- Start with an invariant, not a query
- Use a complete SQL test case template
- Make setup data diagnostic
- Write a validation query that returns failures
A test case said, “Verify the invoice total in the database.” Three testers ran three different queries. One summed invoice lines, one read the invoice header, and one included a voided payment. All marked the case passed. The instruction named a topic but did not define data, grain, business rule, or an observable expected result.
A strong SQL test case is a reproducible argument: given this controlled state and action, this query should return this exact evidence. The examples here use PostgreSQL. Where the case depends on PostgreSQL syntax, that dependency is stated rather than hidden.
Start with an invariant, not a query
Write the rule in business language before choosing tables. “For a finalized invoice, header total equals the sum of non-void lines plus tax, and one ledger entry references that invoice” is a testable invariant. “Run a join on invoices” is not.
Identify five things:
- The event that creates or changes state.
- The entity and grain being checked.
- Fields or related records that may change.
- Records that must not change.
- The exact pass condition.
This prevents a common mistake: validating a copied display field while missing the source-of-truth calculation. Confirm ownership with a developer or data model before freezing the query.
Use a complete SQL test case template
A case must be runnable by someone who did not write it. Keep navigation steps short and spend detail on data identity, timing, and expected rows.
| Field | Example |
|---|---|
| ID and title | DB-INV-014: finalize a three-line invoice |
| Requirement | FIN-82 invoice posting rule |
| Environment | QA PostgreSQL, schema migration 184 |
| Preconditions | clean customer; tax code GB20; worker running |
| Test data | reference SQLTC-0710-014; line amounts 1000, 2500, 650 minor units |
| Action | finalize through POST /invoices/{id}/finalize |
| Validation | query by captured invoice ID and reference |
| Expected | one header, three active lines, total 4980, one ledger row |
| Timing | poll posting status for up to 20 seconds |
| Cleanup | fixture API deletes customer cascade in QA only |
| Evidence | request ID, query output, worker event ID |
Do not paste passwords, connection strings, or personal data into the case. Link to the approved access method and use synthetic values.
Make setup data diagnostic
Choose values that reveal the rule. Three identical lines cannot reveal a join or allocation error as clearly as distinct amounts. Include one value near a boundary and a recognizable reference. Calculate the expectation independently:
Line net values: 1000 + 2500 + 650 = 4150
Tax at 20%: 200 + 500 + 130 = 830
Expected total: 4150 + 830 = 4980 minor unitsThe text block shows the oracle. The database query should confirm 4980, but should not be the only place that number is derived. If rounding is applied at invoice rather than line grain, use data where the two methods differ and state which method the requirement selects.
Create data through supported interfaces unless the purpose is to test a database object directly. Record generated IDs from the response. A test that searches by email and creation minute can collide with parallel runs.
Write a validation query that returns failures
Queries that return only successful rows force a reviewer to remember expectations. Prefer a query that returns zero rows when the invariant holds, with diagnostic columns when it fails.
WITH line_totals AS (
SELECT invoice_id,
count(*) FILTER (WHERE status <> 'VOID') AS active_lines,
sum(net_minor + tax_minor)
FILTER (WHERE status <> 'VOID') AS calculated_total
FROM invoice_lines
GROUP BY invoice_id
), ledger_counts AS (
SELECT source_id AS invoice_id, count(*) AS ledger_rows
FROM ledger_entries
WHERE source_type = 'INVOICE'
GROUP BY source_id
)
SELECT i.invoice_id, i.total_minor, l.calculated_total,
l.active_lines, COALESCE(g.ledger_rows, 0) AS ledger_rows
FROM invoices i
JOIN line_totals l USING (invoice_id)
LEFT JOIN ledger_counts g USING (invoice_id)
WHERE i.invoice_id = 730014
AND (i.status <> 'FINALIZED'
OR i.total_minor <> l.calculated_total
OR l.active_lines <> 3
OR COALESCE(g.ledger_rows, 0) <> 1);This PostgreSQL sql query should return zero rows. Replace 730014 with the captured ID through a safe parameter mechanism. If it returns a row, the selected values show which part of the invariant failed. The filtered aggregate is PostgreSQL syntax; use CASE expressions for engines that do not support FILTER.
State cardinality and values precisely
“Data is correct” is not an expected result. Specify row count, column values, ordering only when meaningful, null behavior, precision, and allowed timing. Examples include:
- Exactly one invoice row has reference
SQLTC-0710-014. - Exactly three non-void line rows belong to the captured invoice ID.
total_minoris 4980, not a formatted currency string.posted_atis non-null and no earlier than the finalize request timestamp.- No ledger row exists before finalization; exactly one exists after completion.
For asynchronous work, avoid a fixed sleep. Define a terminal field, polling interval, maximum duration, and failure output. A 20-second allowance does not mean any timestamp inside 20 seconds is functionally correct. It means the test waits for a deterministic result before declaring a timeout.
Use tolerances only for a documented reason. Money and counts are normally exact. Performance durations and floating-point scientific values may require a range. State both the range and why it is legitimate.
Add negative and non-change assertions
A negative test should prove the absence of side effects, not merely an error message. When finalization is rejected for an invalid tax code, verify the invoice remains draft, no ledger entry exists, line values are unchanged, and no posting event is queued.
SELECT
i.status,
i.total_minor,
count(le.entry_id) AS ledger_rows
FROM invoices i
LEFT JOIN ledger_entries le
ON le.source_type = 'INVOICE' AND le.source_id = i.invoice_id
WHERE i.invoice_id = 730015
GROUP BY i.invoice_id, i.status, i.total_minor;The PostgreSQL sql query should return one row with DRAFT, the pre-action total, and 0. Save the pre-action value in the test case rather than deciding after execution that the current value “looks unchanged.”
Also cover duplicate requests, missing parents, maximum values, null optional fields, forbidden users, and recovery after dependency failure. Each should name which tables remain untouched.
Design safe setup and cleanup
Read-only validation is the default. When direct writes are necessary in an isolated test database, use the least privilege account and mark every record with a run identifier. Test cleanup separately so it cannot remove another run’s data.
Transactions are helpful only when all work shares the same connection. An API server cannot usually see uncommitted setup from the tester’s session, and rolling back the tester session will not undo writes committed by the service. In integration suites, a fixture endpoint, per-test schema, ephemeral database, or owned cleanup job is safer.
Never hide cleanup inside an expected-result query. If cleanup fails, report it independently. Residual data can make the next case pass or fail for the wrong reason.
Capture evidence and diagnose failures
Retain the executed SQL, bound parameters, database and schema version, test reference, request ID, timestamps, actual output, and query duration. Export a small result set as text or CSV with sensitive columns omitted. Screenshots alone make values hard to compare and queries hard to rerun.
When a case fails, classify the boundary before editing the expected result:
| Evidence | Likely investigation |
|---|---|
| header wrong, lines right | total calculation or stale header |
| header and lines right, ledger missing | async worker or event publication |
| two ledger rows, repeated request ID | idempotency handling |
| query changes between runs | unstable data identity or concurrent job |
| only test query is wrong | join grain, filter, or null assumption |
A failed test can reveal a defective query rather than a defective product. Peer review SQL with the same care as automation code.
Review cases for automation readiness
Before automating, check that setup owns its data, identifiers are captured, queries are parameterized, expectations are exact, asynchronous completion is observable, cleanup is scoped, and failure output is diagnostic. Remove manual phrases such as “verify visually in the table.”
Keep SQL assertions close to a user-visible or API assertion. Database state proves persistence, but it does not prove the customer saw the right response. A release-quality case connects request, durable state, downstream effect, and presentation where the risk demands it. That connection is what turns a query into evidence.
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01ISTQB glossary
ISTQB
Shared testing terminology for test design, defects, levels, and lifecycle concepts.
FAQ / QUICK ANSWERS
Questions testers ask
What is an SQL test case?
An SQL test case is a documented database validation check with objective, preconditions, setup data, SQL query, expected result, actual result, and evidence. It verifies data state, calculations, constraints, mappings, stored logic, or reporting values using repeatable database queries.
What should an SQL test case include?
Include test case ID, title, requirement reference, database environment, preconditions, test data, setup steps, validation query, expected rows or values, cleanup notes, actual result, screenshots or exported query output, priority, and defect links when needed.
How do SQL test cases support automation?
Well written SQL test cases define stable setup, exact validation queries, expected values, and cleanup rules. Automation can convert those checks into database assertions, API side effect checks, ETL reconciliation jobs, or regression tests that run after deployments.
Should SQL test cases use exact values or ranges?
Use exact values when business rules are deterministic, such as status, count, tax, or mapped code. Use ranges or tolerances when the system allows timing variation, rounding tolerance, asynchronous processing, or performance thresholds. Document the reason for any tolerance.
How do you avoid unsafe SQL test cases?
Use read only access when possible, isolate test data, run setup only in approved environments, wrap risky changes in transactions, document cleanup, avoid shared customer data, and never run destructive statements against production or production like environments without explicit approval.
RELATED GUIDES
Continue the learning route
GUIDE 01
SQL for Testers: Queries Every QA Engineer Should Know
SQL for testers tutorial covering SELECT, JOIN, GROUP BY, test data checks, database validation, defect evidence, QA examples, and reports too.
GUIDE 02
Database Testing Guide: Validate Data, Queries, and Jobs
Database testing guide for QA teams covering data integrity, CRUD checks, migrations, stored procedures, jobs, reports, SQL evidence, and defects.
GUIDE 03
ETL Testing Tutorial: Validate Data Pipelines End to End
ETL testing tutorial for QA teams covering source to target validation, transformations, reconciliation, data quality, SQL checks, and defects.
GUIDE 04
How to Write Test Cases: Complete Guide with Examples
Learn how to write test cases with practical steps, examples, a QA template, common mistakes, and review tips for reliable software coverage.