A Kotlin/Spring Boot library for managing and loading test data fixtures. It provides a framework for creating reusable fixtures, sharing references between them, purging the database before loading, and triggering events for event-driven setup.
Add the dependency to your build.gradle.kts:
dependencies {
implementation("io.github.kostack:fixtures:<version>")
}Or pom.xml:
<dependency>
<groupId>io.github.kostack</groupId>
<artifactId>fixtures</artifactId>
<version>VERSION</version>
</dependency>| Component | Description |
|---|---|
AbstractFixture |
Base class for all fixtures. Implement load() to set up data |
DataFixtureManager |
Orchestrates fixture loading, database purging, and events |
ReferenceRepository |
In-memory store for sharing objects between fixtures |
Purger |
Interface for clearing the database before loading fixtures |
FixtureLoadEvent |
Spring event published before fixtures are loaded |
LoadFixturesCommand |
Spring Shell command: data-fixtures:load |
Extend AbstractFixture and implement the load() suspend function:
import io.github.kostack.fixtures.AbstractFixture
import org.springframework.stereotype.Component
@Component
class UserFixture(
private val userRepository: UserRepository,
) : AbstractFixture() {
override suspend fun load() {
val user = User(
name = faker.name().fullName(),
email = faker.internet().emailAddress(),
)
userRepository.save(user)
// Share this user so other fixtures can reference it
referenceRepository.setReference("admin-user", user)
}
}fakeris a built-innet.datafaker.Fakerinstance available on the companion object.referenceRepositoryis auto-wired from the base class.
Use ReferenceRepository to pass objects from one fixture to another.
Fixture A — sets a reference:
@Component
class OrganizationFixture(
private val orgRepository: OrganizationRepository,
) : AbstractFixture() {
override suspend fun load() {
val org = orgRepository.save(Organization(name = "Acme Corp"))
referenceRepository.setReference("acme-org", org)
}
}Fixture B — reads that reference:
@Component
class ProjectFixture(
private val projectRepository: ProjectRepository,
) : AbstractFixture() {
override suspend fun load() {
val org = referenceRepository.getReference("acme-org") as Organization
projectRepository.save(Project(name = "Alpha", organization = org))
}
}Note: Fixture loading is sequential, so load order matters. Ensure the fixture that sets a reference is loaded before the one that reads it (e.g., via Spring's
@DependsOnor by ordering beans).
Note:
setReferencethrowsIllegalArgumentExceptionif the same name is registered twice.getReferencethrows if the name does not exist.
Implement the Purger interface to clear your database before fixtures load:
import io.github.kostack.fixtures.Purger
import org.springframework.stereotype.Component
@Component
class MongoPurger(
private val mongoTemplate: ReactiveMongoTemplate,
) : Purger {
override suspend fun purge() {
mongoTemplate.collectionNames
.flatMap { mongoTemplate.dropCollection(it) }
.awaitLast()
}
}The DataFixtureManager calls purger.purge() automatically when loadFixtures(dropDatabase = true) is invoked.
Inject DataFixtureManager and call loadFixtures:
@SpringBootTest
class MyIntegrationTest(
private val dataFixtureManager: DataFixtureManager,
) {
@BeforeEach
fun setUp() = runBlocking {
dataFixtureManager.loadFixtures(dropDatabase = true)
}
@Test
fun `users are loaded`() {
// test against seeded data
}
}To only drop the database without loading fixtures:
dataFixtureManager.dropDatabase()The library registers a Spring Shell command data-fixtures:load:
# Drop the database and load all fixtures (default)
shell:> data-fixtures:load
# Load fixtures without dropping the database
shell:> data-fixtures:load --dropDatabase false
shell:> data-fixtures:load -d falseFixtureLoadEvent is published right before fixtures start loading. Use it to perform additional setup:
import io.github.kostack.fixtures.FixtureLoadEvent
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component
@Component
class CacheWarmupListener {
@EventListener
fun onFixtureLoad(event: FixtureLoadEvent) {
// e.g. clear caches, reset counters, etc.
}
}src/
├── main/kotlin/com/example/
│ ├── User.kt
│ └── UserRepository.kt
└── test/kotlin/com/example/
├── fixtures/
│ ├── UserFixture.kt
│ └── PostFixture.kt
└── UserIntegrationTest.kt
UserFixture.kt
@Component
class UserFixture(
private val userRepository: UserRepository,
) : AbstractFixture() {
override suspend fun load() {
val user = userRepository.save(
User(
name = faker.name().fullName(),
email = faker.internet().emailAddress(),
)
)
referenceRepository.setReference("default-user", user)
}
}PostFixture.kt
@Component
class PostFixture(
private val postRepository: PostRepository,
) : AbstractFixture() {
override suspend fun load() {
val user = referenceRepository.getReference("default-user") as User
postRepository.save(Post(title = faker.lorem().sentence(), author = user))
}
}UserIntegrationTest.kt
@SpringBootTest
class UserIntegrationTest(
private val dataFixtureManager: DataFixtureManager,
private val userRepository: UserRepository,
) {
@BeforeEach
fun setUp() = runBlocking {
dataFixtureManager.loadFixtures(dropDatabase = true)
}
@Test
fun `a user is seeded`() = runBlocking {
val users = userRepository.findAll().collectList().awaitSingle()
assertTrue(users.isNotEmpty())
}
}MIT