Your first read-path policy¶
Run a test that returns the expected order, yet fails because the read path writes to the database. The example uses published QueryAudit 0.6.0 and in-memory H2. You need Java 17+, Git, and network access.
1. Run the unexpected-write failure¶
git clone https://github.com/haroya01/query-audit.git
cd query-audit
./gradlew -p examples/first-audit test -PextraWrite=true --rerun-tasks
The test permits one SELECT and zero INSERTs, UPDATEs, or DELETEs:
The order still has the expected status. The added UPDATE fails the policy:
QueryAudit: readsOnce() exceeded its query budget.
UPDATE: executed 1, expected at most 0.
UPDATE orders SET status = 'NEW' WHERE id = 1
This is the expected failure. A download, compilation, or startup error does not verify capture. Use troubleshooting if you do not see the budget message.
2. Inspect the SQL and call site¶
Open the generated audit:
| Field | Result of the failing run |
|---|---|
outcome | FAIL |
reports[0].summary.totalQueries | 2: one SELECT and one UPDATE |
UPDATE entry in reports[0].queries: sql | UPDATE orders SET status = ? WHERE id = ? |
Its stackTrace | Starts with example.audit.FirstAuditTest.writeOnReadPath:43 |
JSON redacts literal values and retains application call sites. In this release the budget message's first call site can be a JDBC proxy; use the JSON evidence to locate the application code.
3. Remove the write and pass¶
Run the same test without the flag:
The new JSON contains one SELECT and no writes. The query policy and functional assertions both pass.
To try a query-count regression too:
Each run replaces report.json. To verify all three outcomes and retain their reports, run:
The verifier checks Gradle exits, JUnit results, captured SQL, application call sites, and the published artifact version. Results are saved under examples/first-audit/build/reports/first-audit-verification/.
4. Apply the budget to your code¶
| Your setup | Next step |
|---|---|
| Existing Spring Boot database test | Install the starter, then verify capture |
| Plain JUnit 5 / JDBC | Expose the instrumented DataSource |
Keep the test's functional assertions. Add @EnableQueryInspector and @ExpectQueries directly to the test, and make the service or repository use the instrumented DataSource. Choose the SELECT limit for that operation; zero INSERT/UPDATE/DELETE limits protect a read path from those writes.
Budgets are upper bounds. select = 1 also permits zero captured SELECTs; omitted attributes are unchecked. Since 0.7.2, exact = true makes the declared attributes exact counts. Verify a deliberate violation through your real operation before trusting a pass. @EnableQueryInspector keeps findings informational while explicit query budgets still fail.
Complete runnable source
The fixture is prepared before the audit starts. Both flags change the operation under test; the assertions and policy stay the same.
package example.audit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.queryaudit.junit5.EnableQueryInspector;
import io.queryaudit.junit5.ExpectQueries;
import javax.sql.DataSource;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.Test;
@EnableQueryInspector
class FirstAuditTest {
static final DataSource DATA_SOURCE = dataSource();
@Test
@ExpectQueries(select = 1, insert = 0, update = 0, delete = 0)
void readsOnce() throws Exception {
readOne();
if (Boolean.getBoolean("queryAudit.demo.extraQuery")) {
readOne();
}
if (Boolean.getBoolean("queryAudit.demo.extraWrite")) {
writeOnReadPath();
}
}
private void readOne() throws Exception {
try (var connection = DATA_SOURCE.getConnection();
var statement = connection.createStatement();
var result = statement.executeQuery("SELECT status FROM orders WHERE id = 1 LIMIT 1")) {
assertTrue(result.next());
assertEquals("NEW", result.getString("status"));
}
}
private void writeOnReadPath() throws Exception {
try (var connection = DATA_SOURCE.getConnection();
var statement = connection.createStatement()) {
// The returned order is unchanged, but the read path now executes a write.
assertEquals(1, statement.executeUpdate("UPDATE orders SET status = 'NEW' WHERE id = 1"));
}
}
private static DataSource dataSource() {
var h2 = new JdbcDataSource();
h2.setURL("jdbc:h2:mem:firstaudit;DB_CLOSE_DELAY=-1");
// Prepare the fixture before the audited test starts.
try (var connection = h2.getConnection();
var statement = connection.createStatement()) {
statement.execute("CREATE TABLE orders (id INT PRIMARY KEY, status VARCHAR(20))");
statement.executeUpdate("INSERT INTO orders (id, status) VALUES (1, 'NEW')");
} catch (java.sql.SQLException exception) {
throw new IllegalStateException("Cannot prepare the order fixture", exception);
}
return ProxyDataSourceBuilder.create(h2).name("first-audit").build();
}
}
Database and version scope
H2 proves capture and budgets. Index-dependent checks may print a missing-metadata notice; add the MySQL or PostgreSQL module when checking that database's indexes. The example downloads the published release independently of the repository build. Review known limitations before requiring a whole suite in CI.
Keep the policy across changes¶
- Record test counts in a contract file and review intentional changes in the diff.
- Inspect a failed audit using its SQL and call sites.
- Add the first CI check, then require audited tests and compare compatible runs.