Feedback on gradle configuration for `test-support` code
Asked Answered
E

1

7

I have been thinking about this a lot lately and wanted to get some feedback on the idea I had a couple of days ago.

Problem:

In a typical code base, every module has a main and a test source-set. This can work quite well for some time but sooner or later I always stumble upon the situation where I would like to group together a bunch of classes which allow easier testing of code that involves a certain module. A good example would be a set of hamcrest matcher classes for a given module.

  • Assumption 1:
    As hamcrest is a library for test-code, these matchers should not go into the main source-set.

  • Assumption 2: These classes should also not go into the test source-set either, as the dependency on the test source is just a workaround for these classes to be available. One usually does not want a dependency on the actual tests. It is also not recommended (by Netflix) to define a dependency on the test source-set of a project.

Solution 1:

Create a dedicated module that contains these classes in the main source-set and simply define a test-dependency on this module wherever you need them.

This was the approach I went with for quite some time now but I don't really like it.

  • First, I never came up with a nice name, except appending testSupport to the name of the original module which results in names like core-testSupport, persistence-testSupport and so on.

  • Second, it creates a lot of modules and the project-tree gets kind of polluted with these modules.

Solution 2: (The one I would appreciate feedback on)

configurations {
    testSupportCompile.extendsFrom compile
    testSupportRuntime.extendsFrom runtime
}

sourceSets {
    testSupport {
        compileClasspath += sourceSets.main.output + configurations.testSupportCompile
        runtimeClasspath += compileClasspath + configurations.testSupportRuntime
    }
}

task testSupportJar(type: Jar) {
    from sourceSets.testSupport.output
    classifier 'testSupport'
}

artifacts {
    testSupportCompile testSupportJar
}

The above gradle configuration could go in file named testSupport.gradle and be applied to any module that needs this dedicated source-set for providing classes that could be reused in tests.

Defining a dependency would work like this:

testCompile project(path: ':core', configuration: 'testSupportCompile')

I am still kind of new to gradle and researched a lot but I still have a few questions.

  1. I understand that declaring a new source-set automatically creates two configurations: <sourceSet>Compile and <sourceSet>Runtime. What I don't really like about this approach is, that one has to use the testSupportCompile configuration when declaring the dependency. Is there a way to alias this to only testSupport or something like that?

  2. My project compiles fine at the moment. However, I am not sure if I am doing things the right way. How could this configuration be improved?

  3. Are there any other ways of achieving the desired functionality? While researching, I did not really find much on this topic which makes me feel like that I am either using the wrong search terms or doing something stupid that simply should not be done.

I know that this is kind of a broad question but I am not sure on where to get proper feedback on stuff like that except here.

Edging answered 15/11, 2016 at 1:0 Comment(0)
A
1

I have a similar situation and I've been postponing the solution for some time, using various hacks and work-arounds. Your question was a final incentive to look into it.

This is what I ended up with -- EDIT made in cooperation with Thomas:

configurations {
    // create a new configuration and inherit everything from compile
    testlib.extendsFrom compile
}

sourceSets {
    testlib {
        // We will at least need access to our main sourceSet and all dependencies that are declared for our configuration.
        compileClasspath += sourceSets.main.output + configurations.testlib
    }
}

task testlibJar(type: Jar) {
    from sourceSets.testlib.output
    classifier 'testlib'
}

artifacts {
    testlib testlibJar    // include the classes into the new configuration
    archives testlibJar    // optional: include the support JAR into "uploadArchives", so it may also be used in other projects
}

Then, in the depending module, simply use:

dependencies {
    testCompile project(path: ':otherproject', configuration: 'testlib')
}

Note that the (empty) testlibCompile and testlibRuntime configurations are still created (as a result of introducing the new testlib source set), but I believe it is safe to just ignore them.

Also, it is often a case that your project's own test configuration needs to use the testlib (project tests depend on the generic test support). In such a case, you may either add a dependency between two configurations of the same project:

testCompile project(path: ':myproject', configuration: 'testlib')

Or enhance the compile and runtime classpaths individually:

configurations {
    testlib.extendsFrom compile
    testCompile.extendsFrom testlib
}
sourceSets {
    test {
        compileClasspath += sourceSets.testlib.output
        runtimeClasspath += sourceSets.testlib.output
    }
}
Aeromechanic answered 21/11, 2016 at 14:19 Comment(7)
If I interpret your solution correctly, the only difference is the implicity dependency of testLib on test (which seems ok). I will look into it as soon as I am at home.Edging
The main difference is using the testSupport configuration instead of testSupportCompile inside both the configurations and archives section.Aeromechanic
I think I had this as an intermediary solution too, but without the implicit dependency of the test configuration on the testSupport configuration. The problem is, without this implicit dependency, you would need to declare it explicitly in every module which does not work if you don't reuse the configurations that are automatically created by the source set. Btw: You don't need to explicitely set the srcDir of your sourceSet. A srcDir with the name of the configuration is created implicitly.Edging
The statements in my sourceSet setup are needed to be able to declare dependencies for this configuration. For example, you might want to declare a dependency (e.g. hamcrest) only for this configuration. In order for this to work, you need to include the declared dependencies (configurations.testSupportCompile) in the compileClasspath of the created sourceSet.Edging
I guess this whole configuration should ultimately be extracted into a plugin :)Edging
Thanks, you are right with the srcDir. I'am using a little bit non-standard layout here, so I need that. Edit was made for the purpose of publishing it here and I did not realize it is not necessary any more.Aeromechanic
Let us continue this discussion in chat.Edging

© 2022 - 2024 — McMap. All rights reserved.