Reg. Unit testing using Fakes in Compose Multiplat...
# multiplatform
k
Reg. Unit testing using Fakes in Compose Multiplatform. Say we have two modules: •
:core:x
has (commonMain, commonTest) •
:data
It depends on corex and has (commonMain, commonTest) A test in datacommonTest needs a fake impl of class from corex module. Where should I put the fake implementation? 1. In corex:commonMain ? - It will be included in binary, so No 2. In corex:commonTest ? - Unable to access another module's commonTest 3. Somewhere else?
m
test fixtures would be the normal solution. https://docs.gradle.org/current/userguide/java_testing.html#sec:java_test_fixtures I don't know if it is supported by KMP yet. It wasn't when I wanted to do this, so I ended up creating a
:core:x-test-support
library for it and put the code in
commonMain
of that module. Then the test source set for
:data
depended on the
:core:x-test-support
.
👍 1
c
Test fixtures aren't supported by KMP… But yeah you can create your own test support module
n
i have a similar need. but don’t think a test module would work for me b/c the things i’d like to test are
internal
in my :core module. so i really need a test fixture (i think) to expose APIs that work with these internal types.
e
in theory you could create the test fixtures configuration yourself, along the lines of
Copy code
// producer/build.gradle.kts

import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMetadataTarget
import org.jetbrains.kotlin.gradle.plugin.usesPlatformOf

plugins {
    kotlin("multiplatform")
}

kotlin {
    targets.all target@{
        if (this is KotlinMetadataTarget) return@target
        val name = "${name}TestFixtures"
        compilations.create("testFixtures") {
            associateWith(this@target.compilations.getByName("main"))
            this@target.compilations.getByName("test").associateWith(this)
            configurations.consumable("${name}ApiElements") {
                attributes {
                    attribute(Usage.USAGE_ATTRIBUTE, objects.named(if (platformType == KotlinPlatformType.jvm) "java-api-jars" else "kotlin-api"))
                    attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
                }
                extendsFrom(configurations.getByName(apiConfigurationName))
                usesPlatformOf(this@target)
                outgoing {
                    capability("$group:${project.name}-test-fixtures:$version")
                    for (output in output.allOutputs) artifact(output) { builtBy(compileTaskProvider) }
                }
            }
            configurations.consumable("${name}RuntimeElements") {
                attributes {
                    attribute(Usage.USAGE_ATTRIBUTE, objects.named(if (platformType == KotlinPlatformType.jvm) "java-runtime-jars" else "kotlin-runtime"))
                    attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
                }
                extendsFrom(configurations.getByName(implementationConfigurationName))
                extendsFrom(configurations.getByName(runtimeOnlyConfigurationName))
                usesPlatformOf(this@target)
                outgoing {
                    capability("$group:${project.name}-test-fixtures:$version")
                    for (output in output.allOutputs) artifact(output) { builtBy(compileTaskProvider) }
                }
            }
        }
    }
    applyDefaultHierarchyTemplate {
        withSourceSetTree(KotlinSourceSetTree("testFixtures"))
    }
}
Copy code
// consumer/build.gradle.kts

plugins {
    kotlin("multiplatform")
}

kotlin {
    sourceSets {
        commonTest {
            dependencies {
                implementation(project(":producer")) {
                    capabilities {
                        requireCapability("$group:$name-test-fixtures:$version")
                    }
                }
            }
        }
    }
}
🎉 1
(I've only verified that Gradle will run with that build script, not that it actually does what is expected)
tried it out now and it does seem to sorta work
n
this is awesome! works as i needed. thank you! the only thing i noticed is Gradle warning that consuming source sets need explicit dependsOn etc. to ensure load order. not sure if there’s some other way to avoid the consumers needing that much ceremony.
@ephemient, any suggestions for how to fix the implicit dependency issue?
e
not sure what you mean. this works fine for me
n
thanks for following up! i’m AFKB now; but will take a look at your example and share the specific error i see from gradle. which, is sporadic by the way, because it seems due to task execution order.
I'm getting the following error with a setup as follows: core (build.gradle.kts)
Copy code
plugins {
    kotlin("multiplatform")
}

kotlin {
    ...
    applyDefaultHierarchyTemplate()

    sourceSets {
        ...
        commonMain.dependencies {
            ...
        }

        commonTest.dependencies {
            ...
        }

        val jsCommon by creating { dependsOn(commonMain.get()) }

        jsMain.get().dependsOn(jsCommon)

        jvmTest.dependencies {
            ...
        }

        val wasmJsMain by getting {
            dependsOn(jsCommon)
        }
    }

    targets.all target@{
        if (this is KotlinMetadataTarget) return@target
        val name = "${name}TestFixtures"

        compilations.create("testFixtures") {
            associateWith(this@target.compilations.getByName("main"))

            this@target.compilations.getByName("test").associateWith(this)

            configurations.consumable("${name}ApiElements") {
                attributes {
                    attribute(Usage.USAGE_ATTRIBUTE, objects.named(if (platformType == KotlinPlatformType.jvm) "java-api-jars" else "kotlin-api"))
                    attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
                }
                extendsFrom(configurations.getByName(apiConfigurationName))
                usesPlatformOf(this@target)
                outgoing {
                    capability("$group:${project.name}-test-fixtures:$version")
                    for (output in output.allOutputs) artifact(output) { builtBy(compileTaskProvider) }
                }
            }

            configurations.consumable("${name}RuntimeElements") {
                attributes {
                    attribute(Usage.USAGE_ATTRIBUTE, objects.named(if (platformType == KotlinPlatformType.jvm) "java-runtime-jars" else "kotlin-runtime"))
                    attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
                }
                extendsFrom(configurations.getByName(implementationConfigurationName))
                extendsFrom(configurations.getByName(runtimeOnlyConfigurationName   ))
                usesPlatformOf(this@target)
                outgoing {
                    capability("$group:${project.name}-test-fixtures:$version")

                    for (output in output.allOutputs) artifact(output) { builtBy(compileTaskProvider) }
                }
            }
        }
    }
    applyDefaultHierarchyTemplate {
        withSourceSetTree(KotlinSourceSetTree("testFixtures"))
    }
}
controls (build.gradle.kts)
Copy code
plugins {
    kotlin("multiplatform")
}

kotlin {
    ...

    sourceSets {
        ...

        commonMain.dependencies {
            api(projects.core)

            ...
        }

        commonTest.dependencies {
            implementation(kotlin("test-common"))
            implementation(kotlin("test-annotations-common"))
        }

        jvmTest.dependencies {
            implementation(kotlin("test-junit"))
            implementation(libs.bundles.test.libs)

            implementation(projects.core) {
                capabilities {
                    requireCapability("$group:$name-test-fixtures:$version")
                }
            }
        }
    }
}
Copy code
A problem was found with the configuration of task ':controls:jvmTest' (type 'KotlinJvmTest').
  - Gradle detected a problem with the following location: '<...>/Core/build/processedResources/jvm/testFixtures'.
    
    Reason: Task ':controls:jvmTest' uses this output of task ':core:jvmTestFixturesProcessResources' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed.
    
    Possible solutions:
      1. Declare task ':core:jvmTestFixturesProcessResources' as an input of ':controls:jvmTest'.
      2. Declare an explicit dependency on ':core:jvmTestFixturesProcessResources' from ':controls:jvmTest' using Task#dependsOn.
      3. Declare an explicit dependency on ':core:jvmTestFixturesProcessResources' from ':controls:jvmTest' using Task#mustRunAfter.
the issue happens if i build with the
parallel
gradle flag.
e
ah Java resources
try instead of
allOutputs
Copy code
compilations.all compilation@{
    // ...
    for (classesDir in output.classesDirs) artifact(classesDir) { builtBy(compileTaskProvider) }
    if (this@compilation is KotlinJvmCompilation) {
        artifact(output.resourcesDir) { builtBy(processResourcesTaskName) }
    }
(not at computer now, can't check)
n
that seems to have worked! i now have:
Copy code
// ...
outgoing {
    capability("$group:${project.name}-test-fixtures:$version")
    this@target.compilations.all compilation@{
        for (classesDir in output.classesDirs) artifact(classesDir) { builtBy(compileTaskProvider) }
        if (this@compilation is KotlinJvmCompilation) {
            artifact(output.resourcesDir) { builtBy(processResourcesTaskName) }
        }
    }
}