Kotlin, JUnit5, JDBC

I needed to test some Kotlin code that used JDBC. I wanted an embedded SQL database for my JUnit5 tests. I’ve done this in Java a number of times so I dug into my old code and … nothing was a fit. The issues were they were very Java centric and JUnit4 based. Here’s what I rolled on my own.

JDBC Compatible Database

H2 provided an easy solution to that:

 Class.forName("org.h2.Driver")
 val connection = DriverManager.getConnection(
    "jdbc:h2:mem:test", 
    "sa",
    ""
)!!

Kotlin and JUnit5

For Kotlin and JUnit5 I know from experience I wanted to go with implementing an @ExtendWith annotation. I also wanted test classes to have easy access to the resultant connection. Here’s what I did for that:

import org.junit.jupiter.api.extension.ExtendWith
import java.sql.Connection

@ExtendWith(EmbeddedDb::class)
open class WithConnection {
    lateinit var connection: Connection
}

This allowed tests to look like:

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class SomeTest : WithConnection() {
    @Test
    fun `should have a valid connection`() {
        assertThat(connection).isNotNull
    }
}

The meat of the work happens in the EmbeddedDb class:

import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
import java.sql.DriverManager

class EmbeddedDb : BeforeEachCallback, AfterEachCallback {
    override fun beforeEach(context: ExtensionContext) {
        Class.forName("org.h2.Driver")
        val connection = DriverManager.getConnection(
           "jdbc:h2:mem:test", 
           "sa",
           ""
        )!!
        (context.requiredTestInstance as WithConnection)
           .connection = connection
    }
    override fun afterEach(context: ExtensionContext) {
        (context.requiredTestInstance as WithConnection)
           .connection.close()
    }
}

It’s very light weight, all it’s doing is instantiating a connection, and setting the one used by the test classes.

What You Get

With this approach, you’ll get a fresh database connection for every test method, and the connection will be set already if you add a @BeforeEach to your test class, so you can set up your schema there.

JDBC, Generated Keys, Oracle Issues

Recently, using JDBC, I needed to retrieve the generated keys from a row I’d just inserted. For example you’re inserting a word into the following table and want the id:

CREATE TABLE WORDS(ID BIGINT AUTO_INCREMENT, WORD CHAR(20))

Not something I’d done before, but very sane, and the solution was easily found:

public long create(String sql) throws SQLException {
try (
Connection connection = dataSource.getConnection();
Statement statement = connection.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS)) {
if (statement.executeUpdate() == 0) {
throw new SQLException("Insert failed, no rows affected.");
}
try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
if (generatedKeys.next()) {
return generatedKeys.getLong(1);
} else {
throw new SQLException("Creating user failed, no ID obtained.");
}
}
}
}

This worked great in my H2 embedded database where my unit tests ran, and in the MariaDB I ran integration tests against.  However when I used the code against an Oracle database … FAIL … something about a type mismatch.  So what went wrong?

The issue is Oracle stuffs a RowId into every record, not because you ask it to, but because it just does. I’m sure this addition makes all sorts of Oracle magic possible. Generally one just ignores it, or at least I do, but in this case Oracle rubs your nose in its decision.  How? When the code above runs against Oracle, the ResultSet for the generated keys it returns, does not contain the generated key you put in the schema, but the RowId Oracle did!

Of course there is another solution.  The following, while a bit more verbose, works with all the database engines I tested:

public long create(String sql, String[] keyColumns) throws SQLException {
try (
Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql, keyColumns)) {
if (statement.execute() == 0) {
throw new SQLException("Insert failed, no rows affected.");
}
try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
if (generatedKeys.next()) {
return generatedKeys.getLong(1);
} else {
throw new SQLException("Creating user failed, no ID obtained.");
}
}
}
}

Be warned, these snippets aren’t working general solutions, but the exemplified approach is.  What Oracle needs is to be told which columns were generated, otherwise it assumes the RowId, and luckily other database engines are happy to work this way too.

Worth Noting Too

JDBC offers ways to get the generated keys via both Statement and PreparedStatement. Only the PreparedStatement routes seem to work with Oracle, and the generated keys ResultSet in Oracle doesn’t seem to know the column names, only their positions (I’ll research the meta data there when I get a chance).