Multi-Module Support¶
Fakt’s multi-module support enables fake reuse across multiple Gradle modules through dedicated collector modules.
Experimental API
Multi-module support is marked @ExperimentalFaktMultiModule. The API is production-ready but may evolve based on real-world feedback. Explicit opt-in is required.
What is Multi-Module Support?¶
Multi-module support allows you to:
- Generate fakes once in a producer module with
@Fakeinterfaces - Collect fakes in a dedicated collector module
- Use fakes across multiple consumer modules in tests
This eliminates fake duplication and enables clean dependency management in large projects.
Architecture: Producer → Collector → Consumer¶
Fakt’s multi-module pattern uses three distinct roles:
┌─────────────────────────────────────────────────────────────────┐
│ PRODUCER MODULE (:core:analytics) │
│ ════════════════════════════════════════════════════════ │
│ • Contains @Fake annotated interfaces │
│ • Fakt generates fakes at compile-time │
│ • Output: build/generated/fakt/commonTest/kotlin/ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ COLLECTOR MODULE (:core:analytics-fakes) † │
│ ════════════════════════════════════════════════════════ │
│ • Collects generated fakes from producer │
│ • FakeCollectorTask copies fakes with platform detection │
│ • Output: build/generated/collected-fakes/{platform}/kotlin/ │
│ • Published as standard Gradle dependency │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ CONSUMER MODULES (:app, :features:login, etc.) │
│ ════════════════════════════════════════════════════════ │
│ • Depend on collector module in tests │
│ • Use fakes via generated factory functions │
│ • No direct dependency on producer's test code │
└─────────────────────────────────────────────────────────────────┘
† Naming is flexible - can be :analytics-fakes, :analytics-test,
:analytics-test-fixtures, or any name you choose
Quick Setup Example¶
// ┌─────────────────────────────────────────────────────────────────┐
// │ PRODUCER: core/analytics/build.gradle.kts │
// └─────────────────────────────────────────────────────────────────┘
plugins {
kotlin("multiplatform")
alias(libs.plugins.fakt)
}
kotlin {
jvm()
sourceSets.commonMain.dependencies {
implementation(libs.fakt.annotations)
}
}
// ┌─────────────────────────────────────────────────────────────────┐
// │ COLLECTOR: core/analytics-fakes/build.gradle.kts │
// └─────────────────────────────────────────────────────────────────┘
plugins {
kotlin("multiplatform")
alias(libs.plugins.fakt)
}
kotlin {
jvm()
sourceSets.commonMain.dependencies {
api(projects.core.analytics)
}
}
fakt {
@OptIn(ExperimentalFaktMultiModule::class)
collectFakesFrom(projects.core.analytics)
}
// ┌─────────────────────────────────────────────────────────────────┐
// │ CONSUMER: app/build.gradle.kts │
// └─────────────────────────────────────────────────────────────────┘
plugins {
kotlin("multiplatform")
}
kotlin {
jvm()
sourceSets {
commonMain.dependencies {
implementation(projects.core.analytics)
}
commonTest.dependencies {
implementation(projects.core.analyticsFakes)
}
}
}
When to Use Multi-Module?¶
✅ Use Multi-Module When¶
- Multiple modules need the same fakes (e.g.,
core/loggerused by 10+ feature modules) - Publishing fakes as artifacts (Maven Central, internal repository)
- Strict module boundaries (DDD, Clean Architecture, modular monoliths)
- Large teams with module ownership (dedicated teams per module)
- Shared test infrastructure (common fakes for integration tests)
❌ Use Single-Module When¶
- Single module or 2-3 closely related modules
- Fakes only used locally (not shared across modules)
- Small team or early prototyping (prefer simplicity)
- Rapid iteration (multi-module adds slight build overhead)
Setup¶
Prerequisites¶
Before starting, ensure you have:
- ✅ Kotlin Multiplatform or JVM project with multiple Gradle modules
- ✅ Fakt plugin installed (see Getting Started)
- ✅ Basic understanding of Gradle module structure
- ✅ Type-safe project accessors enabled in
settings.gradle.kts
Type-Safe Project Accessors
If you don’t have type-safe accessors enabled, add to settings.gradle.kts:
Then sync Gradle to generate
projects.* accessors.
Tutorial Overview¶
We’ll create a simple multi-module setup:
my-project/
├── core/analytics/ # Producer (defines @Fake interfaces)
├── core/analytics-fakes/ # Collector (collects generated fakes)
└── app/ # Consumer (uses fakes in tests)
Time: ~15 minutes
Step 1: Create Producer Module¶
The producer module contains @Fake annotated interfaces.
// core/analytics/build.gradle.kts
plugins {
kotlin("multiplatform")
alias(libs.plugins.fakt)
}
kotlin {
jvm()
iosArm64()
sourceSets.commonMain.dependencies {
implementation(libs.fakt.annotations)
}
}
Define @Fake interface:
// core/analytics/src/commonMain/kotlin/Analytics.kt
@Fake
interface Analytics {
fun track(event: String)
suspend fun identify(userId: String)
}
Build the module: ./gradlew :core:analytics:build
Verify fakes generated in build/generated/fakt/commonTest/kotlin/
Step 2: Create Collector Module¶
The collector module collects generated fakes and makes them available to other modules. Name it anything (:core:analytics-fakes, :analytics-test, etc.).
// core/analytics-fakes/build.gradle.kts
plugins {
kotlin("multiplatform")
alias(libs.plugins.fakt)
}
kotlin {
jvm() // MUST match producer's targets
iosArm64()
sourceSets.commonMain.dependencies {
api(projects.core.analytics) // CRITICAL: Use api() to expose types
implementation(libs.coroutines) // Add dependencies used by fakes
}
}
fakt {
@OptIn(ExperimentalFaktMultiModule::class)
collectFakesFrom(projects.core.analytics)
}
Key points: Use api() for producer dependency, match all producer targets, declare transitive dependencies.
Build and verify: ./gradlew :core:analytics-fakes:build
Verify fakes collected in build/generated/collected-fakes/commonMain/kotlin/
Naming Flexibility
The collector module can be named anything you prefer:
:core:analytics-fakes✅ (recommended convention):core:analytics-test✅:core:analytics-test-fixtures✅:test:analytics✅:testFixtures:analytics✅
Fakt doesn’t impose any naming convention. Choose what fits your project best.
Step 3: Register Modules in settings.gradle.kts¶
Add both modules to your project:
// settings.gradle.kts
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
rootProject.name = "my-project"
include(":core:analytics")
include(":core:analytics-fakes")
include(":app")
Sync Gradle to generate type-safe accessors (projects.core.analytics, etc.).
Step 4: Use Fakes in Consumer Module¶
Now use the collected fakes in your app or feature modules.
Configure app/build.gradle.kts¶
// app/build.gradle.kts
plugins {
kotlin("multiplatform")
}
kotlin {
jvm()
iosArm64()
iosX64()
iosSimulatorArm64()
sourceSets {
commonMain {
dependencies {
// Main code depends on original interfaces
implementation(projects.core.analytics)
}
}
commonTest {
dependencies {
implementation(kotlin("test"))
// Tests depend on collector module
implementation(projects.core.analyticsFakes)
}
}
}
}
Write a Test¶
// app/src/commonTest/kotlin/com/example/app/AppTest.kt
package com.example.app
import com.example.core.analytics.Analytics
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
class AppTest {
@Test
fun `GIVEN analytics fake WHEN tracking event THEN should capture call`() {
val events = mutableListOf<String>()
val analytics: Analytics = fakeAnalytics {
track { event -> events.add(event) }
identify { userId -> println("User: $userId") }
}
analytics.track("user_login")
analytics.track("user_signup")
assertEquals(listOf("user_login", "user_signup"), events)
assertEquals(2, analytics.trackCallCount.value)
}
@Test
fun `GIVEN analytics fake WHEN identify THEN should call suspend function`() = runTest {
val analytics = fakeAnalytics {
identify { userId -> println("User: $userId") }
}
analytics.identify("user-123")
assertEquals(1, analytics.identifyCallCount.value)
}
}
Run Tests¶
Expected: All tests pass ✅
Step 5: Verify the Setup¶
Build Entire Project¶
Check Generated Code Locations¶
Producer (:core:analytics):
core/analytics/build/generated/fakt/
├── commonTest/kotlin/com/example/core/analytics/
│ ├── FakeAnalyticsImpl.kt
│ ├── fakeAnalytics.kt
│ └── FakeAnalyticsConfig.kt
Collector (:core:analytics-fakes):
core/analytics-fakes/build/generated/collected-fakes/
├── commonMain/kotlin/com/example/core/analytics/
│ ├── FakeAnalyticsImpl.kt
│ ├── fakeAnalytics.kt
│ └── FakeAnalyticsConfig.kt
├── jvmMain/kotlin/ (if JVM-specific fakes exist)
└── iosMain/kotlin/ (if iOS-specific fakes exist)
Consumer (:app):
- No generated code (uses compiled fakes from collector dependency)
Verify IDE Autocomplete¶
In your test file, type fake and verify IDE suggests:
- fakeAnalytics()
If not appearing, try:
1. File → Reload All Gradle Projects
2. File → Invalidate Caches → Invalidate and Restart
Multi-Producer Example¶
For projects with multiple core modules:
1. Create additional producers (logger, auth, etc.) with @Fake interfaces
2. Create corresponding collectors:
// core/logger-fakes/build.gradle.kts
fakt {
@OptIn(ExperimentalFaktMultiModule::class)
collectFakesFrom(projects.core.logger)
}
3. Add all collectors as test dependencies:
// app/build.gradle.kts
commonTest.dependencies {
implementation(projects.core.analyticsFakes)
implementation(projects.core.loggerFakes)
implementation(projects.core.authFakes)
}
4. Compose multiple fakes in tests:
@Test
fun `test using multiple fakes`() = runTest {
val analytics = fakeAnalytics { track { event -> /* ... */ } }
val logger = fakeLogger { info { msg -> /* ... */ } }
val auth = fakeAuthProvider { login { Result.success(User("123")) } }
// Test your use case with composed fakes
}
Configuration Patterns¶
Pattern 1: Type-Safe Project Accessors (Recommended)¶
fakt {
@OptIn(com.rsicarelli.fakt.compiler.api.ExperimentalFaktMultiModule::class)
collectFakesFrom(projects.core.analytics)
}
Benefits:
- IDE autocomplete
- Compile-time safety
- Refactoring support
Pattern 2: String-Based Paths¶
fakt {
@OptIn(com.rsicarelli.fakt.compiler.api.ExperimentalFaktMultiModule::class)
collectFakesFrom(project(":core:analytics"))
}
Use When:
- Type-safe accessors not available
- Dynamic module names
- Cross-project references
Implementation Details¶
How It Works¶
Fakt’s multi-module flow follows three phases:
1. Producer generates fakes at compile-time in test source sets (build/generated/fakt/commonTest/)
2. Collector copies fakes using FakeCollectorTask:
- Discovers generated fakes from producer
- Analyzes package structure to detect target platform (e.g., com.example.jvm.* → jvmMain/)
- Copies fakes to collector’s source sets (build/generated/collected-fakes/commonMain/)
- Registers as source roots for compilation
3. Consumer uses fakes as standard dependencies:
The collector exposes both original interfaces (via api()) and compiled fakes.
Real-World Patterns¶
This producer-collector-consumer pattern is used in production apps and architectural patterns:
- Multi-module Android apps like Now in Android (NIA)
- Clean Architecture projects with strict layer boundaries
- Domain-Driven Design (DDD) module structures
The pattern enables teams to maintain clear module boundaries while sharing test infrastructure efficiently.
Key Benefits¶
- Fake Reuse: Generate fakes once in producer, use across multiple consumer modules
- Clean Dependencies: Standard Gradle dependencies (
implementation(projects.core.analyticsFakes)) - Publishable Artifacts: Collectors are normal modules that can be published to Maven Central or internal repos
- Platform Awareness: Automatic platform detection places fakes in correct KMP source sets (jvmMain, iosMain, commonMain)
- Type Safety: Compile-time errors if interfaces change, preventing broken tests
Next Steps¶
You’ve successfully set up multi-module support! 🎉
Learn More:
- Troubleshooting - Common issues & solutions
- Examples - Production-quality kmp-multi-module example (11 modules)
- Plugin Configuration - Advanced configuration options
- Performance & Optimization - Build performance tuning