Usage Guide¶
Comprehensive reference for using Fakt-generated fakes in your tests. This guide covers everything from basic patterns to advanced features like coroutines, generics, and call tracking.
Quick Start¶
The most common use case—a simple interface with methods:
// src/commonMain/kotlin/com/example/Analytics.kt
import com.rsicarelli.fakt.Fake
@Fake
interface Analytics {
fun track(event: String)
fun identify(userId: String)
}
Using in tests:
// src/commonTest/kotlin/com/example/AnalyticsTest.kt
import kotlin.test.Test
import kotlin.test.assertEquals
class AnalyticsTest {
@Test
fun `GIVEN fake analytics WHEN tracking events THEN captures calls`() {
val events = mutableListOf<String>()
val fake = fakeAnalytics {
track { event -> events.add(event) }
identify { userId -> println("User: $userId") }
}
fake.track("user_signup")
fake.track("user_login")
fake.identify("user-123")
assertEquals(listOf("user_signup", "user_login"), events)
assertEquals(2, fake.trackCallCount.value)
assertEquals(1, fake.identifyCallCount.value)
}
}
Return Types & Default Behaviors¶
Return Values¶
Configure return values for methods:
@Fake
interface UserRepository {
fun getUser(id: String): User?
fun getAllUsers(): List<User>
fun count(): Int
}
Usage:
@Test
fun `GIVEN configured repository WHEN fetching users THEN returns configured values`() {
val testUser = User("123", "Alice")
val fake = fakeUserRepository {
getUser { id ->
if (id == "123") testUser else null
}
getAllUsers { listOf(testUser) }
count { 1 }
}
assertEquals(testUser, fake.getUser("123"))
assertNull(fake.getUser("456"))
assertEquals(1, fake.getAllUsers().size)
assertEquals(1, fake.count())
}
Smart Defaults¶
Fakt generates smart defaults. You only configure what you need:
Using defaults:
@Test
fun `GIVEN unconfigured fake WHEN calling methods THEN uses defaults`() {
val fake = fakeSettings() // No configuration
assertEquals("", fake.getTheme()) // String default: ""
assertEquals(0, fake.getFontSize()) // Int default: 0
assertEquals(false, fake.isEnabled()) // Boolean default: false
}
Override defaults:
@Test
fun `GIVEN configured fake WHEN calling methods THEN uses custom behavior`() {
val fake = fakeSettings {
getTheme { "dark" }
isEnabled { true }
// getFontSize not configured, uses default: 0
}
assertEquals("dark", fake.getTheme())
assertEquals(true, fake.isEnabled())
assertEquals(0, fake.getFontSize()) // Default
}
Nullable Types¶
Handle nullable types naturally with null defaults:
@Fake
interface UserService {
fun findUser(id: String): User?
fun findByEmail(email: String): User?
}
Usage:
@Test
fun `GIVEN user service fake WHEN finding users THEN handles nulls correctly`() {
val testUser = User("123", "Alice")
val fake = fakeUserService {
findUser { id ->
if (id == "123") testUser else null
}
// findByEmail not configured, default: null
}
assertNotNull(fake.findUser("123"))
assertNull(fake.findUser("456"))
assertNull(fake.findByEmail("alice@example.com")) // Default: null
}
Result Types¶
Fakt handles Result<T> with sensible defaults:
@Fake
interface ApiClient {
fun fetchData(id: String): Result<Data>
fun upload(data: Data): Result<Unit>
}
Usage:
@Test
fun `GIVEN API client fake WHEN fetching data THEN returns configured Result`() {
val testData = Data("123", "content")
val fake = fakeApiClient {
fetchData { id ->
if (id == "123") Result.success(testData)
else Result.failure(NotFoundException())
}
upload { data ->
Result.success(Unit)
}
}
val result1 = fake.fetchData("123")
assertTrue(result1.isSuccess)
assertEquals(testData, result1.getOrNull())
val result2 = fake.fetchData("456")
assertTrue(result2.isFailure)
val result3 = fake.upload(testData)
assertTrue(result3.isSuccess)
}
Collection Types¶
Smart defaults for collections:
@Fake
interface Repository {
fun getAll(): List<Item>
fun getTags(): Set<String>
fun getMetadata(): Map<String, String>
}
Usage:
@Test
fun `GIVEN repository fake WHEN fetching collections THEN uses defaults or configured values`() {
val fake = fakeRepository {
getAll { listOf(Item("1"), Item("2")) }
// getTags not configured, default: emptySet()
// getMetadata not configured, default: emptyMap()
}
assertEquals(2, fake.getAll().size)
assertTrue(fake.getTags().isEmpty()) // Default
assertTrue(fake.getMetadata().isEmpty()) // Default
}
Unit Return Type¶
Methods returning Unit get no-op defaults:
Usage:
@Test
fun `GIVEN event bus fake WHEN publishing events THEN executes configured behavior`() {
val receivedEvents = mutableListOf<Event>()
val fake = fakeEventBus {
publish { event -> receivedEvents.add(event) }
// subscribe not configured, default: { } (no-op)
}
fake.publish(Event("test-event"))
fake.subscribe(mockHandler) // No-op default
assertEquals(1, receivedEvents.size)
assertEquals(1, fake.publishCallCount.value)
assertEquals(1, fake.subscribeCallCount.value)
}
Method Parameters¶
Methods with multiple parameters work as expected:
@Fake
interface Calculator {
fun add(a: Int, b: Int): Int
fun divide(numerator: Double, denominator: Double): Double
}
Usage:
@Test
fun `GIVEN calculator fake WHEN calling methods THEN uses provided parameters`() {
val fake = fakeCalculator {
add { a, b -> a + b }
divide { numerator, denominator ->
if (denominator == 0.0) Double.NaN
else numerator / denominator
}
}
assertEquals(5, fake.add(2, 3))
assertEquals(2.5, fake.divide(5.0, 2.0))
assertTrue(fake.divide(10.0, 0.0).isNaN())
}
Suspend Functions¶
Fakt fully supports Kotlin coroutines and suspend functions without any special configuration.
Basic Suspend Functions¶
Suspend functions work naturally in generated fakes:
@Fake
interface ApiClient {
suspend fun fetchData(id: String): Result<Data>
suspend fun upload(data: Data): Result<Unit>
}
Usage in tests:
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertTrue
class ApiClientTest {
@Test
fun `GIVEN API client fake WHEN fetching data THEN suspends correctly`() = runTest {
val testData = Data("123", "content")
val fake = fakeApiClient {
fetchData { id ->
delay(100) // Suspends correctly
Result.success(testData)
}
upload { data ->
delay(50)
Result.success(Unit)
}
}
val result = fake.fetchData("123")
assertTrue(result.isSuccess)
assertEquals(testData, result.getOrNull())
assertEquals(1, fake.fetchDataCallCount.value)
}
}
Suspend + Non-Suspend Mix¶
Interfaces can mix suspend and regular functions:
@Fake
interface UserRepository {
fun getLocalUser(id: String): User?
suspend fun fetchRemoteUser(id: String): Result<User>
suspend fun syncUsers(): Result<Unit>
}
Usage:
@Test
fun `GIVEN repository WHEN calling mixed functions THEN works correctly`() = runTest {
val fake = fakeUserRepository {
getLocalUser { id -> User(id, "Local") }
fetchRemoteUser { id ->
delay(100)
Result.success(User(id, "Remote"))
}
syncUsers {
delay(200)
Result.success(Unit)
}
}
// Regular function (no suspend)
val local = fake.getLocalUser("123")
assertEquals("Local", local?.name)
// Suspend functions
val remote = fake.fetchRemoteUser("456")
val syncResult = fake.syncUsers()
assertTrue(remote.isSuccess)
assertTrue(syncResult.isSuccess)
}
Suspend Properties¶
Properties with suspend getters are supported:
Usage:
@Test
fun `GIVEN async config WHEN loading THEN suspends correctly`() = runTest {
val fake = fakeAsyncConfig {
loadConfig {
delay(50)
mapOf("key" to "value")
}
}
val config = fake.loadConfig()
assertEquals("value", config["key"])
}
Exception Handling¶
Suspend functions can throw exceptions naturally:
@Test
fun `GIVEN API client WHEN fetch fails THEN throws exception`() = runTest {
val fake = fakeApiClient {
fetchData { id ->
delay(100)
throw NetworkException("Connection failed")
}
}
assertFailsWith<NetworkException> {
fake.fetchData("123")
}
}
Coroutine Context¶
Fakt-generated fakes work with all coroutine contexts:
@Test
fun `GIVEN repository WHEN using different dispatchers THEN works correctly`() = runTest {
val fake = fakeRepository {
fetchData { id ->
withContext(Dispatchers.Default) {
// Computation
Data(id)
}
}
}
val data = fake.fetchData("123")
assertEquals("123", data.id)
}
Best Practices for Suspend Functions¶
Use runTest for Suspend Tests¶
Always wrap suspend function tests in runTest:
@Test
fun `test suspend function`() = runTest { // ✅ Required
val fake = fakeApiClient()
fake.fetchData("123")
}
Use delay() for Testing Timing¶
Test timing-sensitive code with delay():
@Test
fun `GIVEN slow API WHEN fetching THEN handles timeout`() = runTest {
val fake = fakeApiClient {
fetchData { id ->
delay(5000) // Simulate slow response
Result.success(Data(id))
}
}
withTimeout(1000) {
assertFailsWith<TimeoutCancellationException> {
fake.fetchData("123")
}
}
}
Properties¶
Fakt generates fakes for both read-only (val) and mutable (var) properties with automatic call tracking.
Read-Only Properties (val)¶
@Fake
interface Config {
val apiUrl: String
val timeout: Int
}
val fake = fakeConfig {
apiUrl { "https://api.example.com" }
timeout { 30 }
}
assertEquals("https://api.example.com", fake.apiUrl)
assertEquals(1, fake.apiUrlCallCount.value)
Mutable Properties (var)¶
Mutable properties track both getter and setter calls separately:
@Fake
interface Settings {
var theme: String
var fontSize: Int
}
val fake = fakeSettings {
theme { "dark" }
fontSize { 14 }
}
// Getter tracking
assertEquals("dark", fake.theme)
assertEquals(1, fake.getThemeCallCount.value)
// Setter tracking
fake.theme = "light"
assertEquals(1, fake.setThemeCallCount.value)
Generics¶
Fakt fully supports generic type parameters at both class and method levels.
Class-Level Generics¶
@Fake
interface Repository<T> {
fun save(item: T): Result<Unit>
fun getAll(): List<T>
}
val fake = fakeRepository<User> {
save { item -> Result.success(Unit) }
getAll { emptyList() }
}
Method-Level Generics¶
@Fake
interface Transformer {
fun <T, R> transform(input: T, mapper: (T) -> R): R
}
val fake = fakeTransformer {
transform { input, mapper -> mapper(input) }
}
Generic Constraints¶
@Fake
interface ComparableRepository<T : Comparable<T>> {
fun findMax(items: List<T>): T?
}
val fake = fakeComparableRepository<Int> {
findMax { items -> items.maxOrNull() }
}
Variance¶
Fakt supports variance modifiers (out, in):
@Fake
interface Producer<out T> {
fun produce(): T
}
@Fake
interface Consumer<in T> {
fun consume(item: T)
}
Call Tracking¶
Every Fakt-generated fake includes automatic, thread-safe call tracking via Kotlin StateFlow.
Basic Call Tracking¶
Every method automatically tracks calls:
Usage:
@Test
fun `GIVEN fake logger WHEN logging messages THEN tracks call counts`() {
val fake = fakeLogger {
log { message -> println(message) }
error { message -> System.err.println(message) }
}
fake.log("Info message")
fake.log("Another info")
fake.error("Error occurred")
assertEquals(2, fake.logCallCount.value)
assertEquals(1, fake.errorCallCount.value)
}
StateFlow Integration¶
Call counters are StateFlow<Int>, enabling reactive testing:
import app.cash.turbine.test
@Test
fun `GIVEN fake WHEN calling methods THEN counter updates reactively`() = runTest {
val fake = fakeRepository()
fake.getUserCallCount.test {
assertEquals(0, awaitItem())
fake.getUser("123")
assertEquals(1, awaitItem())
fake.getUser("456")
assertEquals(2, awaitItem())
}
}
Property Call Tracking¶
Properties track both getter and setter calls:
@Fake
interface Settings {
var theme: String
}
val fake = fakeSettings {
theme { "dark" }
}
val _ = fake.theme // Getter
assertEquals(1, fake.getThemeCallCount.value)
fake.theme = "light" // Setter
assertEquals(1, fake.setThemeCallCount.value)
Thread Safety¶
All call counters are thread-safe via MutableStateFlow.update:
@Test
fun `GIVEN fake WHEN calling from multiple threads THEN counts correctly`() = runTest {
val fake = fakeAnalytics()
withContext(Dispatchers.Default) {
repeat(1000) {
launch {
fake.track("event")
}
}
}
assertEquals(1000, fake.trackCallCount.value)
}
Advanced Patterns¶
Inheritance¶
Fakt handles inherited methods correctly:
interface BaseService {
fun start(): Boolean
fun stop(): Boolean
}
@Fake
interface UserService : BaseService {
fun getUser(id: String): User
}
Usage:
@Test
fun `GIVEN service fake WHEN calling inherited methods THEN works correctly`() {
val fake = fakeUserService {
start { true }
stop { true }
getUser { id -> User(id, "Alice") }
}
assertTrue(fake.start())
assertEquals("Alice", fake.getUser("123").name)
assertTrue(fake.stop())
assertEquals(1, fake.startCallCount.value)
assertEquals(1, fake.getUserCallCount.value)
assertEquals(1, fake.stopCallCount.value)
}
Reconfiguring Fakes¶
You can reconfigure behavior mid-test if needed:
@Test
fun `GIVEN fake WHEN reconfiguring behavior THEN uses new behavior`() {
val fake = fakeUserRepository()
// Initial configuration
fake.configureGetUser { id -> User(id, "Alice") }
assertEquals("Alice", fake.getUser("123").name)
// Reconfigure
fake.configureGetUser { id -> User(id, "Bob") }
assertEquals("Bob", fake.getUser("123").name)
}
Advanced Usage
Reconfiguring via configureXxx() methods is an advanced pattern. Prefer creating new fakes for different test scenarios.
Next Steps¶
Learn More:
- Testing Patterns - GIVEN-WHEN-THEN, isolated fakes, verification strategies
- Multi-Module - Cross-module fake sharing with collector modules
- Migration from Mocks - Migrating from MockK or Mockito
- Performance - Build performance and optimization
Advanced Configuration:
- Plugin Configuration - Compiler plugin configuration and log levels
- Generated Code Reference - Understanding generated fake implementations
- Platform Support - KMP target support and platform-specific patterns