gradlekotlinsource-sets

How to make Kotlin `internal` objects accessible to tests?


My project uses several Gradle source sets for its production code base instead of just main:

This has proven very useful for limiting dependencies and enforcing separation of concern.

It comes with one downside however: we cannot access classes or methods with visibility internal from within test classes. The reason for this is that the Kotlin compiler places every source set in its own "module":

$ find . -name '*.kotlin_module'
./classes/kotlin/domain/META-INF/contact-management_domain.kotlin_module
./classes/kotlin/dal/META-INF/contact-management_dal.kotlin_module
./classes/kotlin/rest/META-INF/contact-management_dal.kotlin_module
./classes/kotlin/test/META-INF/contact-management.kotlin_module
./classes/kotlin/dbUnitTest/META-INF/contact-management_dbUnitTest.kotlin_module

I would like all sourceset to use the same module name "contact-management", as the main sourceset would by default.

I tried to override the name with the compiler option -module-name:

tasks.withType<KotlinCompile> {
    kotlinOptions {
        // place all source sets in the same kotlin module, making objects with 'internal' visibility available to every source sets of this project
        freeCompilerArgs += listOf("-module-name \"contact-management\")
    }
}

Upon running gradlew build, I get

> Task :contact-management:compileDomainKotlin FAILED
e: Invalid argument: -module-name "contact-management"

The reason being that -module-name "contact-management_domain" is set before by the Gradle code invoking the Kotlin compiler as well, but apparently this option is only accepted once.

In a Gradle build, how can I control what is being considered "one module" by the Kotlin compiler?

A related question where the test source set is to be split has no satisfactory answers so far.


Solution

  • You can do that using kotlin compilations. (As far as I understand, a compilation is simply a block of files that are compiled together. A good explanation can be found here)

    When you create a sourceset in gradle, the kotlin plugin creates a compilation under the hood (with the same name as the sourceset).

    What you can do now with compilations is create associations. If a compilation A is associated with another compilation B, source code in A gets access to internal code units of B.

    So in your case, if the test sourceset should get access to the dal sourceset you can simply associate the test compilation with the dal compilation:

    kotlin.target.compilations.getByName("test").associateWith(kotlin.target.compilations.getByName("dal"))
    

    PS: It also works the other way around. If you create compilations explicitly, the corresponding sourcesets are created under the hood. So for custom sourcesets you can create compilations and associate them:

    val domainCompilation = kotlin.target.compilations.create("domain")
    
    val dalCompilation = kotlin.target.compilations.create("dal") {
        associateWith(domainCompilation)
    }
    

    In above example, the sourceset domain will have access to internal code units of the sourceset dal.