How do `testImplementation` and `testRuntimeOnly` relate to the main source set's configurations, and when do you use each?
answer
- testImplementation = test compile + runtime
- testRuntimeOnly = JUnit engine/launcher
- test configs extendsFrom main configs
- production deps auto-inherited by tests
- compileOnly NOT inherited by tests
basics
~20 stestImplementation adds a dependency to the test compile + runtime classpath (e.g. JUnit, Mockito). testRuntimeOnly adds it only to the test runtime classpath (e.g. the JUnit Platform launcher engine). Test classpaths also extend the main ones.
solid answer
~40 sEach source set gets its own configurations. For `test`, the analogues of `implementation`/`runtimeOnly`/`compileOnly` are `testImplementation`, `testRuntimeOnly`, and `testCompileOnly`. `testImplementation` puts a dependency on both the test compile and test runtime classpath — this is where JUnit Jupiter (`junit-jupiter`), AssertJ, Mockito, etc. go because your test source imports them. `testRuntimeOnly` is for things needed only when tests *run*, not compiled against — the canonical example is `org.junit.jupiter:junit-jupiter-engine` (or `junit-platform-launcher`), which the test runtime discovers but your code never imports. Importantly, the test classpaths **extend** the main ones: `testImplementation` extends `implementation`, so your tests automatically see all your `implementation`/`api` dependencies without re-declaring them. You only declare in the `test*` configurations the *extra* things tests need.
code
kotlin · 7 linesdependencies {
implementation("org.slf4j:slf4j-api:2.0.13") // tests see this automatically
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") // imported in tests
testImplementation("org.assertj:assertj-core:3.25.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher") // runtime discovery only
}go deeper
Know JUnit/Mockito go in testImplementation and the JUnit engine in testRuntimeOnly.
Explain the extendsFrom relationship so production deps are inherited, and the compileOnly non-inheritance gotcha.
Discuss per-source-set configuration families and how custom source sets (e.g. integrationTest) replicate this wiring.
Frame standardized test-dependency conventions and BOM/version alignment across many modules.
## Source sets each have their own configuration set Applying the `java` plugin creates a `main` and a `test` source set. Each source set has a parallel family of configurations: | main | test equivalent | |---|---| | `implementation` | `testImplementation` | | `compileOnly` | `testCompileOnly` | | `runtimeOnly` | `testRuntimeOnly` | ### `testImplementation` Use for any dependency your **test source code imports** — JUnit Jupiter API, AssertJ, Mockito, Testcontainers, etc. It's on the test compile classpath (so `import org.junit.jupiter.api.Test;` works) and the test runtime classpath. ### `testRuntimeOnly` Use for things needed when tests **execute** but never imported in test source. The classic case is the JUnit 5 engine/launcher: ```kotlin dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") // or, modern aggregator: // testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") // testRuntimeOnly("org.junit.platform:junit-platform-launcher") } ``` Your tests import the *API* (`@Test`, `assertEquals`), but the *engine* is discovered at runtime by the JUnit Platform — there's no compile-time reference, so it belongs in `testRuntimeOnly`. ### Test configurations extend the main ones This is the key relationship. Gradle wires: - `testImplementation` **extendsFrom** `implementation` - `testRuntimeOnly` **extendsFrom** `runtimeOnly` So every `implementation`/`api`/`runtimeOnly` dependency of `main` is automatically visible to tests. You never re-declare your production dependencies for tests; you only add the *test-specific extras*. (Note: `compileOnly` is **not** inherited, so a `compileOnly` annotation library like Lombok must be re-declared as `testCompileOnly` if tests need it.) ## Summary mental model - import it in a test → `testImplementation` - needed only when tests run, not imported → `testRuntimeOnly` - production deps are inherited automatically — don't repeat them
- Why is `junit-jupiter-engine` declared as `testRuntimeOnly` rather than `testImplementation`?Test source imports only the JUnit API; the engine is discovered and loaded by the JUnit Platform at runtime, with no compile-time reference, so it belongs on the runtime-only test classpath.
- Do you need to re-declare your `implementation` dependencies for tests?No. `testImplementation` extendsFrom `implementation`, so all main `implementation`/`api`/`runtimeOnly` dependencies are inherited by the test classpaths automatically.
saying these in an interview costs you the question
- Re-declaring production dependencies under `testImplementation` (redundant — they're inherited).
- Putting the JUnit engine on `testImplementation` instead of `testRuntimeOnly`.
- Assuming a `compileOnly` dep (e.g. Lombok) is available to tests — it isn't inherited; use `testCompileOnly`.