Testing JDBC and Spring JDBC
Objective
Before an application reaches for an ORM, it talks to a relational database through JDBC — the low-level Java API for opening a connection, running SQL, and reading a ResultSet. Testing such code means pointing it at a database the test controls, typically an in-memory one (H2) created and dropped around each test so runs stay fast and isolated. Spring's JdbcTemplate sits on top of raw JDBC and removes the connection/exception/resource boilerplate, so the same data-access test becomes much shorter while still exercising real SQL against a real (embedded) database.
Use Cases
- Testing a DAO that issues hand-written SQL, by creating the schema, inserting known rows, running the DAO, and asserting on the results — all against an in-memory database.
- Verifying that a query maps
ResultSetcolumns to domain objects correctly (right fields, right order). - Migrating raw-JDBC data access to Spring's
JdbcTemplate/NamedParameterJdbcTemplateand keeping the tests green through the change. - Testing SQL with named parameters (
:name) instead of positional?placeholders, which are easier to read and reorder. - Getting fast, deterministic data-access feedback in CI without provisioning an external database server.
Deep Dive
Raw JDBC data access
Plain JDBC manages everything by hand: open a connection, prepare a statement, iterate the ResultSet, close resources. The book's DAO reads countries from an H2 database:
javapublic List<Country> getCountryList() {
List<Country> countryList = new ArrayList<>();
try {
Connection connection = openConnection();
PreparedStatement statement = connection.prepareStatement("select * from country");
ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {
countryList.add(new Country(resultSet.getString(2), resultSet.getString(3)));
}
statement.close();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
closeConnection();
}
return countryList;
}Testing it against an in-memory database
The test controls the schema: create the table (and seed rows) before each test, drop it after, so every test starts from a known state. Because H2 runs in-process, no server needs to be started:
java@BeforeEach
void setUp() {
TablesManager.createTable(); // CREATE TABLE COUNTRY(...)
// insert known rows
}
@AfterEach
void tearDown() {
TablesManager.dropTable(); // DROP TABLE IF EXISTS COUNTRY
}
@Test
void testGetCountryList() {
List<Country> countries = countryDao.getCountryList();
assertEquals(expectedCountries, countries);
}Spring JDBC: JdbcTemplate removes the boilerplate
JdbcTemplate handles the connection, statement, exception translation, and resource cleanup, so the DAO shrinks to the SQL and a row mapping. NamedParameterJdbcTemplate adds named parameters:
javaNamedParameterJdbcTemplate template = new NamedParameterJdbcTemplate(dataSource);
Map<String, Object> params = Map.of("name", name + "%");
return template.query(
"select * from country where name like :name",
params,
(rs, rowNum) -> new Country(rs.getString("NAME"), rs.getString("CODE_NAME")));The test looks the same as before — it still runs real SQL against the embedded database — but the production code no longer manages connections by hand.
Book vs. today: obsolete driver loading, JdbcDaoSupport, and test slices
Class.forName("org.h2.Driver")is obsolete. The book'sConnectionManagerloads the driver explicitly. Since JDBC 4.0 (Java 6, 2008), drivers on the classpath are auto-registered via theServiceLoadermechanism, so theClass.forName(...)call is unnecessary dead code today:
java// book: manual, no longer needed
Class.forName("org.h2.Driver");
Connection c = DriverManager.getConnection("jdbc:h2:~/country", "sa", "");
// today: the driver registers itself; just get the connection
Connection c = DriverManager.getConnection("jdbc:h2:~/country", "sa", "");Prefer an injected
JdbcTemplateover extendingJdbcDaoSupport. The book's DAOsextends JdbcDaoSupportand callgetJdbcTemplate(). That base class predates constructor injection; modern Spring injects aJdbcTemplate(orNamedParameterJdbcTemplate) directly, which is easier to test and doesn't tie the DAO to a Spring base class:
java@Repository
public class CountryDao {
private final JdbcTemplate jdbcTemplate;
public CountryDao(JdbcTemplate jdbcTemplate) { // injected, no JdbcDaoSupport
this.jdbcTemplate = jdbcTemplate;
}
}Spring Boot has a slice for this.
@JdbcTestloads just the JDBC infrastructure and an embedded database, and@Sqlruns setup scripts, replacing manualcreateTable()/dropTable()plumbing.
Trade-offs
- An in-memory database is fast but not your production database — H2 is not PostgreSQL/Oracle; its SQL dialect, type coercions, and constraint behavior differ, so a test can pass on H2 and the same SQL fail in production (the "impedance mismatch" the book names). For SQL you depend on, integration-test against the real engine (see the Testcontainers note in the JPA/Hibernate concept).
- Raw JDBC leaks resources if you forget
finally— every connection/statement must be closed explicitly, and a missedclose()is a leak the compiler won't catch:
javaPreparedStatement statement = connection.prepareStatement(sql);
// no finally { statement.close(); connection.close(); } → leaked on exceptionJdbcTemplatetrades explicitness for magic — it swallows the connection lifecycle and translatesSQLExceptioninto Spring's uncheckedDataAccessException; convenient, but you no longer see exactly when the connection opens/closes, which matters for transaction-boundary debugging.- Column-index access is fragile — the book's
resultSet.getString(2)breaks silently if theSELECTcolumn order changes;getString("NAME")(or aRowMapper) is more robust:
javanew Country(rs.getString(2), rs.getString(3)); // breaks if the query's column order changes