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 it is also the most permissive dialect: it accepts SQL your production database rejects and hides upsert paths, sequences and identity handling. Dialect-specific behavior deserves the real database, and the database attribute puts a test class on it, in a Testcontainers-managed container that all test classes of the run share:

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

    @Test
    fun `finds owners by last name`(orm: ORMTemplate) {
        // the same test, now running against real PostgreSQL
    }
}

Scripts, parameter injection and per-test rollback work unchanged, so the fast H2 suite and the thorough PostgreSQL suite share their test code. Each class gets a fresh database inside the shared container, so scripts run against an empty database exactly as on H2. Add org.testcontainers:postgresql and the driver to the test classpath; H2 tests need nothing new. The same attribute exists on the @DataStormTest Spring Boot slice.

05Keep going

The reference documentation covers the mechanics in depth: