Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Framework integration points are now instance-scoped (#198), and the Ktor integr
- SQL failures raised by Storm translate to Spring's `DataAccessException` hierarchy in Spring applications: `SpringExceptionMapper` (storm-spring) translates on vendor error codes with `SQLException` subclass and SQL state fallback, auto-configured by both starters (`storm.exception-translation.enabled=false` to disable) and applied by the `SpringOrmTemplate.of` / `springOrmTemplate` compositions. Failures without a `SQLException` cause keep Storm's own exceptions.
- The Spring Boot starters bind query observations automatically: with an `ObservationRegistry` bean present (Actuator provides one), every query executed by the auto-configured template reports as a `storm.query` Micrometer Observation via the shared `StormObservationAutoConfiguration`. The starters ship storm-micrometer; override the convention with an `ObservationConvention` bean, replace the binding with a `QueryObserver` bean, or disable via `management.observations.enable.storm.query=false`.
- `@EnableStormRepositories(basePackages = ...)` (storm-spring, usable from both language stacks): switches on repository scanning in plain Spring applications, mirroring `@EnableJpaRepositories`; without packages, the annotated class's package is scanned. In Spring Boot the annotation doubles as the explicit override: the auto-configured scanning backs off. The `RepositoryBeanFactoryPostProcessor` adapters also gain configuring constructors (base packages, template bean name, prefix), so multi-template applications define one bean per repository set instead of one subclass.
- `@DataStormTest` test slice in the new storm-spring-boot-test-autoconfigure module, the counterpart of `@DataJpaTest`: starts only the `DataSource`, Storm's auto-configuration, and SQL initialization (plus Flyway/Liquibase when present), keeps regular components out, replaces the data source with an embedded database on Spring Boot 3 (opt out via `spring.test.database.replace=none` for Testcontainers `@ServiceConnection` setups), and rolls back a per-test transaction. One module serves both starters and both Spring Boot 3 and 4: the slice imports the starters' identically named auto-configuration classes, lists relocated platform classes as optional imports, and registers its type exclusion through a context customizer instead of the relocated `@TypeExcludeFilters`.

### Changed

Expand Down
51 changes: 51 additions & 0 deletions docs/spring-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,57 @@ A generic DataSource proxy can only time statements. Storm knows the entity and

Customization follows the usual back-off rules: contribute an `ObservationConvention<StormQueryObservationContext>` bean to override the naming and key values, define your own `QueryObserver` bean to replace the binding entirely, or disable the observation at the registry level with `management.observations.enable.storm.query=false`.

## Testing with @DataStormTest

`@DataStormTest` (from `storm-spring-boot-test-autoconfigure`, test scope) is the Storm test slice, the counterpart of `@DataJpaTest`: it starts only the `DataSource`, Storm's auto-configuration (template, repository scanning, transaction integration, schema validation, exception translation), and SQL initialization, plus Flyway or Liquibase when present. It complements [`@StormTest`](testing.md), which tests data logic without a Spring context: use `@StormTest` for fast query-level tests, and the slice when the test should see what production Spring code sees — injected repository beans, translated exceptions, Spring-managed transactions, and your `storm.*` configuration. Regular `@Component`, `@Service`, and `@Controller` beans stay out of the context. Each test method runs in a transaction that is rolled back afterwards, so tests cannot see each other's writes.

On Spring Boot 3 the application's `DataSource` is replaced with an embedded in-memory database by default; put a `schema.sql` (and optionally `data.sql`) on the test classpath to initialize it, or let Flyway or Liquibase (included in the slice) create the schema as in production. On Spring Boot 4 the replacement activates when the `spring-boot-jdbc-test-autoconfigure` artifact is present; without it, point the slice at a database with the `properties` attribute:

```kotlin
@DataStormTest
class VisitRepositoryTest(
@Autowired private val visitRepository: VisitRepository,
) {

@Test
fun `finds all visits`() {
visitRepository.count() shouldBe 14
}

@Test
fun `insert rolls back after the test`() {
visitRepository.insert(Visit(description = "temporary"))
}
}
```

To run against a real database instead, disable the replacement (`spring.test.database.replace=none` on Spring Boot 3; the default without the test-database artifact on Spring Boot 4) and hand the slice a Testcontainers-managed database through `@ServiceConnection`:

```kotlin
@DataStormTest(properties = ["spring.test.database.replace=none"])
@Testcontainers
class VisitRepositoryPostgresTest(
@Autowired private val visitRepository: VisitRepository,
) {

companion object {
@Container
@ServiceConnection
@JvmStatic
val postgres = PostgreSQLContainer("postgres:17-alpine")
}

@Test
fun `finds all visits`() {
visitRepository.count() shouldBe 14
}
}
```

The slice works with both starters — it pulls in the starter's auto-configuration classes by name, which are identical for the Java and Kotlin stacks — and with both Spring Boot 3 and 4: where Spring Boot moved classes between the releases, the slice is exclusion-based rather than annotation-composed. The annotation supports `properties` for per-test configuration (for example `properties = ["storm.validation.schema-mode=none"]` when the test schema deliberately diverges from the entity model) and `includeFilters`/`excludeFilters` to pull additional components into the slice.

Two composition notes. Test fixtures loaded per test method participate in the rollback transaction, so identity-generated keys drift across methods (sequences do not roll back); load reference fixtures once with `@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_CLASS)` and let each test's own writes roll back. And an application that excludes `StormTransactionAutoConfiguration` (the coroutine-native setup) should re-enable it for the slice with `properties = ["spring.autoconfigure.exclude="]` — without the transaction bridge, repository writes cannot join the rollback transaction.

## Multiple Data Sources

Larger applications sometimes expose the same database through several `DataSource` beans: one connection pool per domain, each with its own pool size, timeout, and isolation settings, so heavy batch work cannot starve interactive traffic. Storm supports this topology with one `ORMTemplate` per `DataSource` and one repository post-processor per domain.
Expand Down
2 changes: 2 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Writing tests for database code can involve repetitive setup: creating a `DataSo
The module provides two categories of functionality:

1. **JUnit 5 integration** (`@StormTest`) for automatic database setup, script execution, and parameter injection.

Spring Boot applications additionally have the [`@DataStormTest` slice](spring-integration.md#testing-with-datastormtest), which boots the Storm part of the Spring context instead of bypassing it: repositories arrive as Spring beans, exceptions arrive translated, and each test rolls back a Spring-managed transaction. `@StormTest` stays the fastest option for query-level tests; the slice covers the Spring wiring.
2. **Statement capture** (`SqlCapture`) for recording and inspecting SQL statements generated during test execution. This component is framework-agnostic and works independently of JUnit.

---
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
<module>storm-kotlin-spring</module>
<module>storm-spring-boot-starter</module>
<module>storm-kotlin-spring-boot-starter</module>
<module>storm-spring-boot-test-autoconfigure</module>
<module>storm-ktor</module>
<module>storm-ktor-test</module>
</modules>
Expand Down
5 changes: 5 additions & 0 deletions storm-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@
<artifactId>storm-kotlin-spring-boot-starter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>st.orm</groupId>
<artifactId>storm-spring-boot-test-autoconfigure</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>st.orm</groupId>
<artifactId>storm-micrometer</artifactId>
Expand Down
6 changes: 6 additions & 0 deletions storm-kotlin-spring-boot-starter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,12 @@
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>st.orm</groupId>
<artifactId>storm-spring-boot-test-autoconfigure</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package st.orm.spring.boot.autoconfigure

import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestMethodOrder
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import st.orm.spring.boot.autoconfigure.slice.Visit
import st.orm.spring.boot.autoconfigure.slice.VisitRepository
import st.orm.spring.boot.test.DataStormTest

/**
* Verifies that the shared @DataStormTest slice works against the Kotlin starter: the slice imports resolve
* to the Kotlin starter's auto-configuration classes, repositories bind through the Kotlin adapter, and each
* test rolls back.
*/
@TestMethodOrder(OrderAnnotation::class)
@DataStormTest
class DataStormTestSliceTest(
@Autowired private val visitRepository: VisitRepository,
@Autowired private val applicationContext: ApplicationContext,
) {

@Test
@Order(1)
fun `repositories are scanned through the Kotlin adapter`() {
visitRepository.count() shouldBe 2
applicationContext.getBean(st.orm.spring.AbstractRepositoryBeanFactoryPostProcessor::class.java)
.shouldBeInstanceOf<AutoConfiguredRepositoryBeanFactoryPostProcessor>()
}

@Test
@Order(2)
fun `writes are rolled back per test`() {
visitRepository.insert(Visit(description = "written inside the test transaction"))
visitRepository.count() shouldBe 3
}

@Test
@Order(3)
fun `previous test's write is gone`() {
visitRepository.count() shouldBe 2
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package st.orm.spring.boot.autoconfigure

import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
open class SliceTestApplication
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package st.orm.spring.boot.autoconfigure.slice

import st.orm.Entity
import st.orm.PK

@JvmRecord
data class Visit(
@PK val id: Int = 0,
val description: String? = null,
) : Entity<Int>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package st.orm.spring.boot.autoconfigure.slice

import st.orm.repository.EntityRepository

interface VisitRepository : EntityRepository<Visit, Int>
2 changes: 2 additions & 0 deletions storm-kotlin-spring-boot-starter/src/test/resources/data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
INSERT INTO visit (description) VALUES ('rabies shot');
INSERT INTO visit (description) VALUES ('checkup');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE visit (id INT AUTO_INCREMENT PRIMARY KEY, description VARCHAR(255));
153 changes: 153 additions & 0 deletions storm-spring-boot-test-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>st.orm</groupId>
<artifactId>storm-framework</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>storm-spring-boot-test-autoconfigure</artifactId>
<name>Storm Spring Boot Test AutoConfigure</name>
<description>Test slice support for Storm in Spring Boot applications, providing the @DataStormTest annotation.</description>
<url>https://github.com/storm-repo/storm-framework</url>
<licenses>
<license>
<name>The Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<developers>
<developer>
<name>Leon van Zantvoort</name>
<email>storm@zantvoort.biz</email>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/storm-repo/storm-framework.git</connection>
<developerConnection>scm:git:ssh://github.com/storm-repo/storm-framework.git</developerConnection>
<url>https://github.com/storm-repo/storm-framework/</url>
</scm>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<phase>none</phase>
</execution>
<execution>
<id>default-testCompile</id>
<phase>none</phase>
</execution>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<release>${java.version}</release>
<compilerArgs>--enable-preview</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
@{argLine}
--enable-preview
--add-opens java.base/java.lang=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>st.orm.spring.boot.test.autoconfigure</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<executions>
<execution>
<id>attach-javadocs</id>
<configuration>
<legacyMode>true</legacyMode>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>st.orm</groupId>
<artifactId>storm-spring-boot-starter</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Loading
Loading