https://github.com/sya-ri/fiktion
Kotlin fake data for tests that should read like intent, not fixture setup.
https://github.com/sya-ri/fiktion
kotlin kotlin-compiler kotlin-compiler-plugin kotlin-js kotlin-library kotlin-multiplatform kotlin-native kotlin-test kotlin-testing test testing
Last synced: about 1 month ago
JSON representation
Kotlin fake data for tests that should read like intent, not fixture setup.
- Host: GitHub
- URL: https://github.com/sya-ri/fiktion
- Owner: sya-ri
- License: mit
- Created: 2026-05-27T17:56:17.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-28T20:08:41.000Z (about 1 month ago)
- Last Synced: 2026-05-28T22:05:05.739Z (about 1 month ago)
- Topics: kotlin, kotlin-compiler, kotlin-compiler-plugin, kotlin-js, kotlin-library, kotlin-multiplatform, kotlin-native, kotlin-test, kotlin-testing, test, testing
- Language: Kotlin
- Homepage: http://fiktion.s7a.dev
- Size: 597 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# Fiktion
[](https://github.com/sya-ri/fiktion/actions/workflows/ci.yml)
[](https://central.sonatype.com/artifact/dev.s7a/fiktion-core)
[](https://plugins.gradle.org/plugin/dev.s7a.fiktion)
Fiktion is a Kotlin Multiplatform fake data library for tests that should read like intent, not fixture setup.
It creates complete, realistic-enough object graphs from your Kotlin types, then lets each test override only the values
that matter. No annotations in production code. No builders for every model. No fixture files drifting away from the
domain.
```kotlin
@Test
fun `paid orders are shipped`() {
val order = fake {
Order::status generates OrderStatus.Paid
// Other values are filled in automatically.
}
shippingService.ship(order)
assertEquals(
ShipmentStatus.Created,
shipmentRepository.findByOrderId(order.id).status
)
}
```
Fiktion is built for the common testing problem where the object shape matters, but most individual values do not:
```kotlin
val user = fake {
User::id generates "user-1"
}
```
You get:
- object graph generation for ordinary Kotlin classes
- scoped overrides for the fields a test actually asserts
- deterministic output with seeds
- collection, map, nullable, default-value, enum, sealed, object, class, data class, and value class support
- add-ons for external libraries, including common Java/JVM standard library types
## Status
Fiktion is pre-release. The core behavior is usable, but API names and compiler-generated metadata internals may change
before 1.0.
The repository currently contains:
- `fiktion-core`: runtime APIs and built-in generators
- `fiktion-compiler-plugin`: Kotlin compiler plugin for generated type metadata
- `fiktion-gradle-plugin`: Gradle wiring for test source sets
- `fiktion-addon-java`: rules for common Java/JVM standard library types
## Install
Apply the Gradle plugin and add the runtime to your test dependencies:
```kotlin
plugins {
kotlin("jvm") version "2.3.21"
id("dev.s7a.fiktion") version "0.2.3"
}
dependencies {
testImplementation("dev.s7a:fiktion-core:0.2.3")
// Optional: common JVM types such as Instant, UUID, URI, and Java collections.
testImplementation("dev.s7a:fiktion-addon-java:0.2.3")
}
```
Fiktion is enabled for test source sets by default, including JVM `test` and Multiplatform source sets such as
`commonTest` and `jvmTest`.
## Basic Usage
Generate a value with `fake()`:
```kotlin
val text = fake()
val count = fake()
val user = fake()
```
Use a seed when a test needs repeatable data:
```kotlin
val first = fake(seed = 123)
val second = fake(seed = 123)
check(first == second)
```
Override only what the test cares about:
```kotlin
val user = fake {
User::id generates "user-1"
User::displayName generates "Test User"
}
```
Rules inside `fake { ... }` apply to that generated object graph only, so one test does not accidentally configure
another model.
## Nested Properties
Nested objects can be configured inline:
```kotlin
val user = fake {
User::profile {
Profile::nickname generates "example"
}
}
```
You can also target a nested path directly:
```kotlin
val user = fake {
(User::profile / Profile::nickname) generates "example"
}
```
Name rules are useful when the same convention appears across several types:
```kotlin
val user = fake {
name("id") generates "user-1"
name(".*Name".toRegex()) generatesBy { "generated-name" }
}
```
## Collections And Maps
Collections can generate each element automatically:
```kotlin
val catalog = fake {
Catalog::items generates auto withSize 3
}
```
Or you can provide element rules:
```kotlin
val catalog = fake {
Catalog::tags generatesEach {
string(length = 8)
} withSize (1..5)
}
```
Maps support key, value, and entry generation:
```kotlin
val index = fake {
SearchIndex::entries generatesKeys { string(length = 8) }
SearchIndex::entries generatesValues { fake() }
SearchIndex::aliases generatesOneOf listOf("primary", "secondary")
}
```
## Nulls And Defaults
Nullable values can be generated by probability:
```kotlin
val user = fake {
User::nickname generates "nickname" orNullAt 0.3
}
```
Constructor defaults can also be selected when Fiktion has metadata for the type:
```kotlin
val user = fake {
User::profile generates Profile(nickname = "generated") orDefaultAt 30.percent
}
```
`Double` probabilities use `0.0..1.0`. Percentage helpers are available with `percent`.
When a name rule intentionally generates `null`, declare the value type explicitly:
```kotlin
name("nickname") generates null
name("nickname") generatesBy { null }
```
## Reusable Configuration
Use `Fiktion { ... }` when a test suite needs a local generator configuration:
```kotlin
val fiktion = Fiktion {
type() generatesBy {
User(id = "api-user-${random.nextLong()}")
}
}
val user = fiktion.fake()
```
Use `Fiktion.configure` for process-wide test configuration. It returns a snapshot so the previous configuration can be
restored:
```kotlin
val snapshot = Fiktion.configure {
name("email") generatesBy {
"test-${random.nextInt()}@example.test"
}
}
try {
val user = fake()
} finally {
check(snapshot.restore())
}
```
`Fiktion.configure` is useful when the same defaults should apply to top-level `fake()` calls across a test project
or framework-managed test context:
```kotlin
val snapshot = Fiktion.configure {
name("email") generatesBy {
"user-${random.nextLong()}@example.test"
}
name("id") generatesBy {
"id-${random.nextLong()}"
}
}
```
Because the configuration is global, prefer `fake { ... }` or `Fiktion { ... }` for rules that only one test needs.
When global configuration is installed by a test hook, restore the snapshot in the matching teardown hook. `restore()`
returns `false` if another `Fiktion.configure` call has installed a newer global configuration; use `restore(force =
true)` only from cleanup code that owns the whole test process or project-level configuration.
Rule precedence is:
1. Per-call rules
2. Fiktion instance rules
3. Global rules
4. Add-on rules
5. Built-in rules
Within the same precedence level, more specific targets win before registration order. When two matching rules have the
same specificity, the later registration wins.
## Test Framework Integration
With `kotlin.test`, prefer an isolated `Fiktion { ... }` instance from `@BeforeTest` when each test should start from
the same defaults. Use `Fiktion.configure` with `@BeforeTest` / `@AfterTest` only when the test intentionally needs
top-level `fake()` calls to see global rules:
```kotlin
import dev.s7a.fiktion.Fiktion
import dev.s7a.fiktion.fake
import kotlin.test.BeforeTest
import kotlin.test.Test
class UserServiceTest {
private lateinit var fiktion: Fiktion
@BeforeTest
fun configureFiktion() {
fiktion = Fiktion {
name("email") generatesBy {
"user-${random.nextLong()}@example.test"
}
}
}
@Test
fun `creates a user`() {
val user = fiktion.fake()
// test body
}
}
```
With JUnit 5, `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` only shares one test class instance across methods in
that class. Prefer an isolated instance for class-scoped defaults:
```kotlin
import dev.s7a.fiktion.Fiktion
import dev.s7a.fiktion.fake
import dev.s7a.fiktion.generators.string
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserRepositoryTest {
private lateinit var fiktion: Fiktion
@BeforeAll
fun configureFiktion() {
fiktion = Fiktion {
name("email") generatesBy {
"${string(length = 12)}@example.test"
}
}
}
@Test
fun `finds user by email`() {
val user = fiktion.fake()
// test body
}
}
```
Use `Fiktion.configure` when the rules intentionally need to affect global `fake()` calls, for example project-wide
test helpers or framework hooks that do not receive a `Fiktion` instance. Since this mutates process-wide state, keep it
at project-level setup when tests may run in parallel.
For JUnit 5, put process-wide defaults in an extension and store the snapshot in the root extension store:
```kotlin
import dev.s7a.fiktion.Fiktion
import dev.s7a.fiktion.FiktionSnapshot
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.ExtensionContext
class FiktionExtension : BeforeAllCallback {
override fun beforeAll(context: ExtensionContext) {
context.root
.getStore(ExtensionContext.Namespace.GLOBAL)
.getOrComputeIfAbsent(FiktionResource::class.java) {
FiktionResource()
}
}
private class FiktionResource : AutoCloseable {
private val snapshot: FiktionSnapshot =
Fiktion.configure {
name("email") generatesBy {
"user-${random.nextLong()}@example.test"
}
}
override fun close() {
check(snapshot.restore(force = true))
}
}
}
```
With Kotest, prefer an isolated instance for spec-scoped defaults. Kotest specs are `SingleInstance` by default, so a
property initialized in the spec body is shared by tests in that spec:
The same pattern works with other Kotest spec styles such as `FunSpec`, `DescribeSpec`, `FreeSpec`, `ShouldSpec`, and
`BehaviorSpec`; keep the `Fiktion { ... }` instance at the spec scope and call `fiktion.fake()` from tests.
```kotlin
import dev.s7a.fiktion.Fiktion
import dev.s7a.fiktion.fake
import io.kotest.core.spec.style.StringSpec
class UserRepositorySpec : StringSpec({
val fiktion = Fiktion {
name("email") generatesBy {
"user-${random.nextLong()}@example.test"
}
}
"finds user by email" {
val user = fiktion.fake()
// test body
}
})
```
Kotest project configuration is useful only when global defaults should affect top-level `fake()` across the whole
test project:
```kotlin
package io.kotest.provided
import dev.s7a.fiktion.Fiktion
import dev.s7a.fiktion.FiktionSnapshot
import io.kotest.core.config.AbstractProjectConfig
class ProjectConfig : AbstractProjectConfig() {
private lateinit var fiktionSnapshot: FiktionSnapshot
override suspend fun beforeProject() {
fiktionSnapshot = Fiktion.configure {
name("email") generatesBy {
"user-${random.nextLong()}@example.test"
}
}
}
override suspend fun afterProject() {
check(fiktionSnapshot.restore(force = true))
}
}
```
## Rule Targets
Configuration scopes support type, property, path, and name targets:
```kotlin
type() generatesBy { /* any User */ }
property() generatesBy { /* any String property owned by User */ }
property("id") generates "user-1"
name("id") generates "shared-id"
name("email") generatesBy { "test-${random.nextInt()}@example.test" }
property(User::profile / Profile::nickname) generates "example"
User::id generates "user-1"
```
Property references such as `User::id generates "user-1"` are the concise form for owner-specific property rules.
`property("id")` is the equivalent explicit form when the owner and value type should be spelled out.
## Compiler Plugin
The compiler plugin generates runtime metadata for Kotlin types in enabled source sets. This is what lets Fiktion create
objects without annotations.
Supported shapes include:
- regular classes and data classes with supported primary constructors
- value classes with one constructor value
- enum classes
- sealed classes and sealed interfaces
- singleton objects and companion objects
Shapes that should be configured explicitly are skipped:
- abstract classes and interfaces
- fun interfaces and annotation classes
- inner classes and local classes
- classes without a primary constructor
- private or protected primary constructors
- vararg or otherwise unsupported constructor parameters
Value class overrides depend on what the test wants to control. For a public underlying property, target that property:
```kotlin
@JvmInline
value class UserId(val value: String)
val userId = fake {
UserId::value generates "user-1"
}
```
For a private underlying property, use a name rule because Kotlin code outside the class cannot reference the property:
```kotlin
@JvmInline
value class UserId(private val value: String)
val userId = fake {
name("value") generates "user-1"
}
```
When the whole value object should be fixed, prefer a type rule in shared configuration:
```kotlin
val fiktion = Fiktion {
type() generates UserId("user-1")
}
```
Skipped types can still be generated with explicit rules:
```kotlin
val fiktion = Fiktion {
type() generates PrivateUser.create("user-1")
}
```
## Gradle Configuration
Test source sets are enabled by default. Main/runtime source sets are opt-in:
```kotlin
fiktion {
sourceSet("commonMain") {
enabled.set(true)
}
}
```
Project-wide controls are also available:
```kotlin
fiktion {
// Enable every Kotlin source set.
enabled.set(true)
// Disable the default test-source-set behavior.
testEnabled.set(false)
sourceSet("jvmTest") {
enabled.set(true)
}
}
```
## Java Add-On
Add `fiktion-addon-java` when tests need common JVM types such as `java.time`, `java.util`, `java.net`, `java.nio`,
`java.sql`, or `java.util.concurrent` types:
```kotlin
dependencies {
testImplementation("dev.s7a:fiktion-addon-java:0.2.3")
}
```
With the Gradle plugin enabled, add-ons on the compilation classpath are registered automatically before `fake()`
calls:
```kotlin
import java.time.Instant
import java.util.UUID
val instant = fake()
val uuid = fake()
```
Add-ons can still be installed explicitly when the compiler plugin is not enabled for that source set:
```kotlin
val fiktion = Fiktion {
install(JavaFiktionAddon)
}
```
## Custom Add-Ons
Add-ons are reusable bundles of rules:
```kotlin
public object CustomFiktionAddon : FiktionAddon {
override val id: String = "custom"
override fun install(builder: FiktionAddonBuilder) {
with(builder) {
type() generatesBy {
Token(value = string(length = 32))
}
}
}
}
```
To make a third-party add-on auto-registerable, include a resource file named `META-INF/fiktion/addons` in the add-on
artifact. Each non-empty line should contain one add-on object class name:
```text
com.example.fiktion.ExampleFiktionAddon
```
Installed add-ons sit below explicit per-call, instance, and global rules in precedence.
## Realistic Data
Fiktion core focuses on object graph generation, rule resolution, and Kotlin metadata. Strict domain data such as names,
email addresses, postal addresses, or localized text can come from custom rules, add-ons, or libraries such as Datafaker.
```kotlin
val faker = Faker()
val user = fake {
User::email generatesBy {
faker.internet().emailAddress()
}
}
```
## Development
Local `./gradlew build` skips browser test execution unless `-Pfiktion.enableBrowserTests=true` is provided. CI enables
browser tests and installs Chrome before running Gradle.