In a multi-module build, how do you make an integrationTest suite in one module depend on another module's production classes, and what are the trade-offs of doing so?
answer
- implementation(project(":domain"))
- resolves apiElements/runtimeElements
- java-library hides implementation deps
- build-graph coupling cost
- prefer public API / fixtures
basics
~10 sUse implementation(project(":otherModule")) inside the suite's dependencies block. It adds a project dependency on that module's outgoing API/runtime variants. The trade-off is tighter build-graph coupling and recompilation when the dependency changes.
solid answer
~40 sInside the suite's `dependencies { }` you pass a project path: `implementation(project(":domain"))`. This wires the integration suite against `:domain`'s published API/runtime variants exactly like any normal project dependency, so the suite can exercise cross-module behavior end to end. Trade-offs: it adds an edge to the build dependency graph, so changing `:domain` triggers recompilation/retest of the integration suite, and it can blur module boundaries if integration tests start reaching deep into another module's internals. Prefer depending on the other module's **public API** (use `java-library` so `api` vs `implementation` is honored) or its published **test fixtures** (`testFixtures(project(":domain"))`) for shared test scaffolding, rather than over-coupling. For genuine black-box integration you may prefer the application/runtime artifact over compile-time project deps.
code
kotlin · 11 linestesting {
suites {
val integrationTest by registering(JvmTestSuite::class) {
dependencies {
implementation(project())
implementation(project(":domain"))
implementation(testFixtures(project(":domain")))
}
}
}
}go deeper
Know that project(":name") targets another module; deeper trade-offs not expected.
Explain how the cross-module dependency resolves and that fixtures share test scaffolding.
Weigh build-graph coupling, API vs implementation scope, and recommend depending on public surfaces/fixtures over deep coupling.
Set org-wide policy on cross-module test dependencies: favor black-box integration against runtime artifacts/public APIs to keep the build graph and module boundaries maintainable at scale.
## Declaring the cross-module dependency The suite dependency DSL accepts project paths: ```kotlin testing { suites { val integrationTest by registering(JvmTestSuite::class) { dependencies { implementation(project()) // own main implementation(project(":domain")) // another module's main implementation(testFixtures(project(":domain"))) // its shared fixtures } } } } ``` `project(":domain")` resolves `:domain`'s outgoing `apiElements`/`runtimeElements` variants. With the `java-library` plugin, only its `api`-scoped transitive deps leak to consumers; `implementation`-scoped ones stay hidden — important for clean boundaries. ## What it costs - **Build-graph coupling:** an edge `:thisModule:integrationTest -> :domain`. A change in `:domain` invalidates the suite's compile and test tasks. With many such edges, incremental builds touch more of the graph. - **Boundary erosion:** if integration tests import internal classes of another module (possible if that module exposes too much), you couple tests to implementation details, making refactors painful. - **Variant selection surprises:** depending on the wrong variant (e.g. needing runtime classes but getting only API) shows up as `NoClassDefFoundError` at test runtime — usually fixed with `runtimeOnly` or by depending on the right configuration. ## Healthier patterns 1. **Depend on the public API only** — keep `:domain` a `java-library` and consume its `api`. 2. **Share test scaffolding via fixtures** — `testFixtures(project(":domain"))` instead of reaching into its `test` source set (you cannot consume another project's `test` output directly without exposing it as a variant anyway). 3. **Black-box integration** — for true end-to-end tests, depend on the assembled runtime artifact (e.g. the bootJar or a published module) and drive it via its external surface rather than compile-time project deps. ## Takeaway `implementation(project(":x"))` in the suite block is the direct way to test across modules, but each such edge is a coupling decision: prefer public APIs and published fixtures, and reserve deep project dependencies for cases where the integration genuinely needs them.
- Can the integrationTest suite directly consume another module's `test` source set output?Not by default — a project's test code isn't exposed as a consumable variant. To share test code across modules you publish it via `java-test-fixtures` and consume `testFixtures(project(":x"))`.
- Why does using `java-library` matter when depending on another module?It distinguishes `api` (transitively visible) from `implementation` (hidden) dependencies, so consumers — including your integration suite — only see the intended public surface, keeping module boundaries clean.
- How might a wrong variant choice surface at runtime?As a `NoClassDefFoundError` or `ClassNotFoundException` during test execution when runtime-only classes weren't on the resolved classpath; fix by using `runtimeOnly` or the correct configuration.
saying these in an interview costs you the question
- Suggesting you can depend on another module's `test` output directly without test fixtures.
- Ignoring the build-graph coupling cost and recommending deep project dependencies everywhere.
- Confusing `api` and `implementation` scope effects on what the suite can see transitively.