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 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
Library Purpose Version kotlin-test Core testing framework 2.3.0 kotlin-test-junit JUnit integration 2.3.0 junit JUnit 4 4.13.2 androidx-testExt-junit AndroidX test extensions 1.3.0 androidx-espresso-core Android UI testing 3.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
Common Tests
Android Tests
iOS Tests
Desktop 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
Open test file in IntelliJ IDEA or Android Studio
Click the green arrow next to test function
Select “Debug” instead of “Run”
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