Skip to main content
GreenhouseAdmin uses Kotlin’s multiplatform testing capabilities to ensure code quality across all platforms. This guide covers test structure, running tests, and best practices.

Test Structure

The project follows Kotlin Multiplatform’s source set structure for tests:
composeApp/src/
├── commonTest/          # Shared tests (all platforms)
│   └── kotlin/
│       └── com/apptolast/greenhouse/admin/
│           └── ComposeAppCommonTest.kt
├── androidUnitTest/     # Android-specific unit tests
├── androidInstrumentedTest/  # Android instrumentation tests
├── iosTest/             # iOS-specific tests
├── jvmTest/             # Desktop-specific tests
├── jsTest/              # JavaScript-specific tests
└── wasmJsTest/          # WebAssembly-specific tests

Common Tests

Shared tests in commonTest/ run on all platforms. These should test:
  • Business logic in ViewModels
  • Repository implementations
  • Data transformations
  • Utility functions
  • Domain models
Example: composeApp/src/commonTest/kotlin/com/apptolast/greenhouse/admin/ComposeAppCommonTest.kt
package com.apptolast.greenhouse.admin

import kotlin.test.Test
import kotlin.test.assertEquals

class ComposeAppCommonTest {
    @Test
    fun example() {
        assertEquals(3, 1 + 2)
    }
}

Platform-Specific Tests

Platform-specific test source sets are used for:
  • Platform API integrations
  • UI tests (Android instrumentation)
  • Platform-specific utilities
  • Expect/actual implementations

Test Dependencies

From build.gradle.kts:113-115 and libs.versions.toml:
commonTest.dependencies {
    implementation(libs.kotlin.test)  // kotlin-test framework
}

Available Testing Libraries

LibraryPurposeVersion
kotlin-testCore testing framework2.3.0
kotlin-test-junitJUnit integration2.3.0
junitJUnit 44.13.2
androidx-testExt-junitAndroidX test extensions1.3.0
androidx-espresso-coreAndroid UI testing3.7.0

Running Tests

Run All Tests

Execute tests across all platforms:
# Run all tests
./gradlew test

# Run with detailed output
./gradlew test --info

# Run with stack traces
./gradlew test --stacktrace

Run Platform-Specific Tests

# Run shared tests on JVM
./gradlew :composeApp:jvmTest

# Run shared tests on JS
./gradlew :composeApp:jsTest

# Run shared tests on Wasm
./gradlew :composeApp:wasmJsTest

Continuous Testing

Run tests automatically on file changes:
# Continuous testing mode
./gradlew test --continuous

# Or use --watch (if available)
./gradlew test -t

Writing Tests

ViewModel Testing

Test ViewModels using the repository pattern with mock repositories:
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest

class ClientViewModelTest {
    
    private class FakeClientRepository : ClientRepository {
        override suspend fun getClients(): Result<List<Client>> {
            return Result.success(listOf(
                Client(id = "1", name = "Test Client")
            ))
        }
    }
    
    @Test
    fun `loadClients should update state with clients`() = runTest {
        // Arrange
        val repository = FakeClientRepository()
        val viewModel = ClientViewModel(repository)
        
        // Act
        viewModel.loadClients()
        
        // Assert
        assertEquals(1, viewModel.state.value.clients.size)
        assertEquals("Test Client", viewModel.state.value.clients[0].name)
    }
}

Repository Testing

Test repository implementations with mock API services:
import kotlin.test.Test
import kotlin.test.assertTrue
import kotlinx.coroutines.test.runTest

class ClientRepositoryImplTest {
    
    private class FakeApiService : ApiService {
        override suspend fun getClients(): List<ClientDto> {
            return listOf(
                ClientDto(id = "1", name = "Test Client")
            )
        }
    }
    
    @Test
    fun `getClients should return success with client list`() = runTest {
        // Arrange
        val apiService = FakeApiService()
        val repository = ClientRepositoryImpl(apiService)
        
        // Act
        val result = repository.getClients()
        
        // Assert
        assertTrue(result.isSuccess)
        assertEquals(1, result.getOrNull()?.size)
    }
}

Utility Function Testing

import kotlin.test.Test
import kotlin.test.assertEquals

class DateUtilsTest {
    
    @Test
    fun `formatDate should format ISO date correctly`() {
        val isoDate = "2024-03-15T10:30:00Z"
        val formatted = formatDate(isoDate)
        
        assertEquals("Mar 15, 2024", formatted)
    }
    
    @Test
    fun `formatDate should handle invalid date`() {
        val invalidDate = "invalid"
        val formatted = formatDate(invalidDate)
        
        assertEquals("", formatted)
    }
}

Composable Testing (Android)

Test Compose UI components using Compose testing APIs:
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.Test

class LoginScreenTest {
    
    @get:Rule
    val composeTestRule = createComposeRule()
    
    @Test
    fun loginButton_isDisplayed() {
        composeTestRule.setContent {
            LoginScreen(
                onLoginClick = {},
                onRegisterClick = {}
            )
        }
        
        composeTestRule
            .onNodeWithText("Login")
            .assertIsDisplayed()
    }
    
    @Test
    fun loginButton_clickTriggersCallback() {
        var clicked = false
        
        composeTestRule.setContent {
            LoginScreen(
                onLoginClick = { clicked = true },
                onRegisterClick = {}
            )
        }
        
        composeTestRule
            .onNodeWithText("Login")
            .performClick()
        
        assert(clicked)
    }
}

Testing Best Practices

1. Follow AAA Pattern

Structure tests with Arrange-Act-Assert:
@Test
fun `test description`() {
    // Arrange - Set up test data and dependencies
    val repository = FakeRepository()
    val viewModel = MyViewModel(repository)
    
    // Act - Execute the code under test
    viewModel.performAction()
    
    // Assert - Verify the results
    assertEquals(expected, actual)
}

2. Use Descriptive Test Names

// Good
@Test
fun `getClients should return empty list when API returns no data`()

// Bad
@Test
fun test1()

3. Test One Behavior Per Test

// Good - Tests one specific behavior
@Test
fun `login should show error when password is empty`()

@Test
fun `login should show error when username is empty`()

// Bad - Tests multiple behaviors
@Test
fun `login validation tests`()

4. Use Fake Implementations Instead of Mocks

For multiplatform code, prefer fake implementations over mocking frameworks:
// Good - Works on all platforms
class FakeRepository : Repository {
    var shouldFail = false
    
    override suspend fun getData(): Result<Data> {
        return if (shouldFail) {
            Result.failure(Exception("Failed"))
        } else {
            Result.success(Data())
        }
    }
}

// Avoid - Mocking frameworks may not be multiplatform
// val mockRepository = mockk<Repository>()

5. Test Error Cases

@Test
fun `getClients should return failure when network error occurs`() = runTest {
    val repository = FakeRepository().apply { shouldFail = true }
    val viewModel = ClientViewModel(repository)
    
    viewModel.loadClients()
    
    assertTrue(viewModel.state.value.error != null)
}

6. Use Coroutine Testing Utilities

For testing suspend functions:
import kotlinx.coroutines.test.runTest

@Test
fun `async operation completes successfully`() = runTest {
    val result = repository.fetchData()
    assertTrue(result.isSuccess)
}

Test Coverage

Generate Coverage Reports

# Run tests with coverage
./gradlew test jacocoTestReport

# View report
# Open build/reports/jacoco/test/html/index.html

Coverage Goals

  • ViewModels: 80%+ coverage
  • Repositories: 80%+ coverage
  • Utilities: 90%+ coverage
  • UI Components: Focus on critical paths

Continuous Integration

Tests are automatically run in CI/CD pipeline. From .github/workflows/ci-cd.yml:27-67:
build:
  name: Build & Test
  runs-on: ubuntu-latest
  
  steps:
    - name: Checkout code
      uses: actions/checkout@v4
    
    - name: Set up JDK 21
      uses: actions/setup-java@v4
      with:
        java-version: '21'
        distribution: 'temurin'
        cache: 'gradle'
    
    - name: Build WASM Distribution
      run: ./gradlew :composeApp:wasmJsBrowserDistribution --no-daemon --stacktrace
Tests run automatically on every push and pull request to develop and main branches.

Debugging Tests

Run Single Test

# Run specific test class
./gradlew test --tests "com.apptolast.greenhouse.admin.ComposeAppCommonTest"

# Run specific test method
./gradlew test --tests "*.ComposeAppCommonTest.example"

Enable Debug Logging

# Run with debug output
./gradlew test --debug

# Run with info level
./gradlew test --info

Debug in IDE

  1. Open test file in IntelliJ IDEA or Android Studio
  2. Click the green arrow next to test function
  3. Select “Debug” instead of “Run”
  4. Set breakpoints as needed

Test Utilities

Common Test Utilities Location

Create shared test utilities in commonTest/kotlin/:
commonTest/kotlin/
└── com/apptolast/greenhouse/admin/
    ├── fakes/           # Fake implementations
    │   ├── FakeRepository.kt
    │   └── FakeApiService.kt
    ├── fixtures/        # Test data builders
    │   └── ClientFixtures.kt
    └── utils/           # Test utilities
        └── TestUtils.kt

Example Test Fixture

// fixtures/ClientFixtures.kt
object ClientFixtures {
    fun createClient(
        id: String = "test-id",
        name: String = "Test Client",
        status: ClientStatus = ClientStatus.ACTIVE
    ) = Client(
        id = id,
        name = name,
        status = status
    )
    
    fun createClients(count: Int = 3) = List(count) {
        createClient(id = "client-$it", name = "Client $it")
    }
}

Next Steps

Deployment

Learn how to deploy your tested application

Architecture

Understand the architecture you’re testing

Build docs developers (and LLMs) love