skip to content

Inside a testFixtures source set, when should a dependency go in testFixturesApi versus testFixturesImplementation, and what's the visibility difference for consumers?

level: middleimportance: should knowfreq 28%

answer

  1. mirror of java-library api/implementation
  2. api = leaks to consumer compile classpath
  3. implementation = runtime-only for consumers
  4. rule: public signature mentions the type?
  5. base test class -> api; payload builder lib -> implementation

basics

~10 s

Use testFixturesApi when a fixture's public signature exposes a type from that dependency (it then leaks onto consumers' compile classpath). Use testFixturesImplementation for internal-only deps that consumers shouldn't compile against.

solid answer

~40 s

The `java-test-fixtures` plugin mirrors the `api`/`implementation` split from `java-library` but for the fixtures source set. `testFixturesImplementation` puts a dependency on the fixtures' own compile/runtime classpath but **hides** it from consumers — it's transitive at runtime only. `testFixturesApi` does the same *and* exposes the dependency on the consumer's test **compile** classpath, because a fixture's public API mentions a type from it. The rule is the same as ordinary API leakage: if a `public` fixture method parameter, return type, or extended/implemented type comes from dependency X, X must be `testFixturesApi` so consumers can compile against those signatures. Otherwise prefer `testFixturesImplementation` to keep the fixtures' transitive compile graph small, which speeds up consumers' compilation and avoids accidental coupling. AssertJ used only inside fixture bodies is `testFixturesImplementation`; a base test class that returns a `org.junit.jupiter.api.Extension` is `testFixturesApi`.

code

kotlin · 7 lines
kotlin
dependencies {
    // Consumers extend a base test class returning a JUnit Extension -> leaks
    testFixturesApi("org.junit.jupiter:junit-jupiter-api:5.10.2")

    // Only used inside fixture method bodies -> hidden from consumers
    testFixturesImplementation("com.fasterxml.jackson.core:jackson-databind:2.17.0")
}

go deeper

for a junior

Knowing both configurations exist and that one is for shared/public types is enough.

for a middle

Apply the leakage rule confidently with a concrete example of each.

for a senior

Tie it back to compile-avoidance and recompilation cascades; reason about ABI leakage.

for a principal

Set conventions: minimize the fixtures' public API surface across the org, audit testFixturesApi usage in shared modules.

## Recap: api vs implementation The `java-library` plugin distinguishes: - `api` — dependencies that *leak* into your public type signatures, so anything depending on you must also compile against them. - `implementation` — internal dependencies, present at runtime for you but **not** on your consumers' compile classpath. Keeping the `api` surface minimal reduces consumer compile classpaths and recompilation cascades. ## The test-fixtures equivalents `java-test-fixtures` creates the same pair for the `testFixtures` source set: ```kotlin dependencies { testFixturesApi("org.junit.jupiter:junit-jupiter-api:5.10.2") // leaks to consumers' test compile testFixturesImplementation("org.assertj:assertj-core:3.25.3") // hidden from consumers } ``` ## The decision rule Ask: *does a `public` member of any fixture class mention a type from this dependency?* - **Yes** (parameter type, return type, public field type, a superclass/interface a fixture extends, an annotation on a public element that consumers must see): use `testFixturesApi`. Consumers need that type on their test **compile** classpath to use the fixture. - **No** — the dependency is only referenced inside method bodies or private members: use `testFixturesImplementation`. Consumers still get it transitively at **runtime** (so the fixtures actually run) but it's absent from their compile classpath. ## Why it matters Over-using `testFixturesApi` re-creates the classic problem: every consumer recompiles when an internal dependency of the fixtures changes, and consumers can accidentally start using internal libraries. A common case: a fixtures module provides an abstract `BaseIntegrationTest` extending a JUnit base type — that base type must be `testFixturesApi`. But a JSON library used only to build sample payloads inside a factory method stays `testFixturesImplementation`. ## Practical note Fixtures also implicitly get the module's **own** `main` classes (compile + runtime) and can declare regular `testFixturesRuntimeOnly` deps for runtime-only needs. The `api`/`implementation` discipline you already apply to production code carries over unchanged.

  • A fixture has a private helper that uses Jackson; which configuration?
    `testFixturesImplementation` — nothing in the public signature mentions Jackson, so it shouldn't leak to consumers' compile classpath.
  • Will a `testFixturesImplementation` dependency be available when the consumer actually runs its tests?
    Yes — it's still transitive at runtime; it's only hidden from the consumer's *compile* classpath.

saying these in an interview costs you the question

  • Putting everything in `testFixturesApi` 'to be safe' — it bloats consumer compile classpaths and causes needless recompiles.
  • Assuming `testFixturesImplementation` deps are missing at consumer test runtime — they are present transitively at runtime.

context