Adding resources from external JAR in separate configuration

I am in the process of creating a Gradle plugin that starts a separate WireMock process and loads stub mappings for our tests. In order to promote decoupling the WireMock runtime logic from loading stub mappings I am trying to create a separate configuration where developers can add their own stub mappings via external JAR files.

class WireMockPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val extension = project.extensions.create("wireMockPlugin", WireMockPluginExtension::class.java)
        project.configurations.create("wiremockResources")

        project.tasks.register("startWireMock", StartWireMockTask::class.java) {
            wireMockPort.set(extension.wireMockPort.convention(9999))
        }
        [...]
    }
}

The plugin has an extension for configuring the port and some shared logic for starting and stopping on a specific port but that is not relevant for my question. The @TaskAction in startWireMock calls a private function for loading all stub mappings from configuration “wiremockResources”:

    @TaskAction
    protected fun startWireMockTask() {
        [...]
        loadStubMappings()

        val location = project.layout.buildDirectory.file("wiremock").get()

        ProcessBuilder()
            .directory(project.layout.projectDirectory.asFile)
            .command("cmd", "/c", "start", "java", "-jar", getWireMockJar().absolutePath, "--root-dir", location.asFile.absolutePath, "--port", wireMockPort.get().toString())
            .start()
    }

    private fun loadStubMappings() {
        project.configurations.getByName("wiremockResources").forEach {
            JarFile(it).use { jar ->
                jar.entries().asSequence()
                    .filter { entry -> entry.name.startsWith("stubs/") && entry.name.endsWith(".json") }
                    .forEach { entry ->
                        val name = entry.name.substringAfterLast("/")
                        val content = jar.getInputStream(entry).bufferedReader().use { it.readText() }

                        project.layout.buildDirectory.file("wiremock/mappings/$name").get().asFile.apply {
                            parentFile.mkdirs()
                            writeText(content)
                        }
                    }
            }
        }
    }

Unfortunately, I am unable to test this with TestKit (using JUnit), when I add the resource containing some test mappings to my dependencies in configuration wiremockResources, no mappings are loaded:

    fun `it should be possible to load external mapping files from JAR`() {
        File(tempDir, "build.gradle.kts").apply {
            """
                   plugins {
                       `java`
                       id("<package>.wiremock-plugin")
                   }
  
                   dependencies {
                       testImplementation("org.wiremock:wiremock-standalone:3.12.1")
                       
                       wiremockResources("<package>:test-wiremock-mappings:0.1.0-SNAPSHOT")
                   }
            """.trimIndent()
        }

        startWireMock()

The strangest part of it all is that when I add the same dependency to the plugin logic, like so:

class WireMockPlugin : Plugin<Project> {
    override fun apply(project: Project) {
[...]
        project.configurations.create("wiremockResources")
        project.dependencies.add("wiremockResources", "<package>:test-wiremock-mappings:0.1.0-SNAPSHOT")
[...]
}

It does seem to work fine (the 2 JSON mappings in that externally prepared JAR file in resources/ are loaded perfectly fine). Is this a restriction with TestKit or am I overlooking something here?

Take note: There is no specific output as the process starts fine. I already tried to debug through the loading of the configurations and am under the impression that it might have something to do with the fact that the configuration is not resolved (yet). But even when I call project.configurations.getByName("wiremockResources").resolve() it doesn’t seem to find any files on the wiremockResources configuration for some reason.

In the meantime I found out that when I publish + consume the plugin in another project and add the wiremock stub mappings JAR on the wiremockResources configuration of that project, everything works perfectly fine.

So I am under the impression that most likely, this is peculiar behavior of either TestKit, or with my test setup that does not seem to work as expected.

Hard to say from reading the dry code, maybe an MCVE could help.

But from what you described, it sounds like one of the typical class loading problems.
If you use withPluginClasspath() of TestKit, the plugin classes are injected through some parent class loader which - depending on the code and case - sometimes behaves differently from consuming the published plugin.

If that is the case in your case, one way is to publish to a local directory-based maven repository (optimally not mavenLocal()) and consume the plugin from that repository in your functional tests instead of using withPluginClasspath().

This is a bit more work to set up, but a more production-like setup and for some cases the only way to properly test.


Btw. using a task to fire up WireMock or whatever else is in almost all cases a bad idea.
For example if the test task is up-to-date or cached, WireMock is started for nothing and if tests fail you have to make sure to still tear down WireMock.

The idiomatic way is to use a shared build service that implements AutoCloseable. In its constructor you will start the server and in its close() method you will tear down the server. Then you declare the test tasks to use that service. That way the server will be started only if necessary and will be shut down after the last task that needs it is finished and before the build ends.

Thanks for your quick reply. Indeed, we use withPluginClasspath to inject the plugin classes. We do this on purpose, as it seems to be only way to test our classes without first releasing a complete new artifact and using it in our test code (which sidesteps the purpose of testing before releasing anway).

We might be able to change that setup to publish, then test. What would be the main reason to not use mavenLocal() for that purpose?

You are probably right regarding the setup of using a task vs. using shared build service. I didn’t know of the idiomatic way you described. I will check it with our internal Gradle experts how to set that up.

We do this on purpose, as it seems to be only way to test our classes without first releasing a complete new artifact and using it in our test code (which sidesteps the purpose of testing before releasing anway).

No it is not, I just described you the alternative way. :slight_smile:
I said publish to a local repository, not release. :wink:

What would be the main reason to not use mavenLocal() for that purpose?

Because mavenLocal() is not suitable for any purpose and should be avoided wherever possible. It is broken by design in Maven already, and makes builds slow, unreliable, and flaky at best. Whenever you use it although you shouldn’t, you should at least put it last in the list and always have a repository content filter controlling exactly what is taken from it. You can find more detailed information also at Declaring repositories.