Tutorials/Testing the data layer

Testing your data layer with @StormTest

Data-layer tests usually start with a page of setup: a DataSource, schema scripts, wiring. Storm's test module reduces that to one annotation, and then lets you assert something most stacks cannot: the SQL itself.

Series · The Storm way4 min readKotlin

01The task

Test repository and query code against a real database schema, fast enough to run on every build, without booting a dependency-injection container or writing connection plumbing in every test class.

02One annotation

@StormTest creates an in-memory H2 database, runs your schema and data scripts, and injects test method parameters. No Spring context, no base class, no manual setup:

OwnerRepositoryTest.kt Kotlin · storm-test
1
2
3
4
5
6
7
8
9
10
@StormTest(scripts = ["/schema.sql", "/data.sql"])
class OwnerRepositoryTest {

    @Test
    fun `finds owners by last name`(orm: ORMTemplate) {
        val owners = orm.entity<Owner>().findAll(Owner_.lastName eq "Smith")

        owners.size shouldBe 2
    }
}

Parameters resolve by type: ORMTemplate, DataSource, SqlCapture, or any type with a static of(DataSource) factory. Each test starts from the scripted state, and the whole cycle runs in milliseconds because nothing heavier than JUnit is involved.

03Assert the SQL, not just the rows

A test that only checks results can pass while the query underneath quietly degrades. SqlCapture records every statement a block generates, with its operation type and bound parameters, so query shape becomes an assertion:

PetQueryTest.kt Kotlin · storm-test
1
2
3
4
5
6
7
8
@Test
fun `pet list loads owners in the same query`(orm: ORMTemplate, capture: SqlCapture) {
    val pets = capture.execute { orm.entity<Pet>().findAll() }

    pets.forEach { render(it.owner.lastName) }      // walk the graph freely
    capture.count(Operation.SELECT) shouldBe 1   // an added query fails the build
    capture.count(Operation.UPDATE) shouldBe 0   // reads stay reads
}

This turns performance properties into regression tests: "no N+1", "this service only reads", "the batch runs one statement" all become one-line assertions that fail the build when violated.

04The same tests against PostgreSQL

H2 keeps the loop fast, but dialect-specific behavior deserves the real database. @StormTest picks up a static dataSource() method, which is exactly the hook Testcontainers needs:

PostgresOwnerTest.kt Kotlin · storm-test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@StormTest(scripts = ["/schema-postgres.sql", "/data.sql"])
@Testcontainers
class PostgresOwnerTest {

    companion object {
        @Container
        val postgres = PostgreSQLContainer("postgres:latest")

        @JvmStatic
        fun dataSource(): DataSource = PGSimpleDataSource().apply {
            setUrl(postgres.jdbcUrl)
            user = postgres.username
            password = postgres.password
        }
    }

    // the same tests now run against real PostgreSQL
}

Scripts and parameter injection work unchanged, so the fast H2 suite and the thorough PostgreSQL suite share their test code.

05Keep going

The reference documentation covers the mechanics in depth: