skip to content

Explain the difference between testFixturesApi and testFixturesImplementation. When should a fixture dependency go in each?

level: middleimportance: should knowfreq 38%

answer

  1. mirrors java-library api/implementation
  2. Api leaks to consumer compile classpath
  3. Implementation = runtime-only for consumers
  4. type-in-signature → Api
  5. default to Implementation

basics

~20 s

testFixturesApi dependencies are part of the fixtures' public API and leak onto consumers' compile classpath; testFixturesImplementation ones are internal to the fixtures and only reach consumers at runtime. Put a library in Api only if its types appear in fixture signatures.

solid answer

~40 s

The `java-test-fixtures` plugin mirrors the `java-library` `api`/`implementation` split for fixtures. **`testFixturesApi`** declares dependencies whose types appear in the *public surface* of your fixtures — e.g. a fixture method returns an AssertJ `AbstractAssert` or takes a domain type from another library. Those leak transitively onto the compile classpath of any module consuming the fixtures, because the consumer needs them to compile. **`testFixturesImplementation`** declares dependencies used only inside fixture bodies (a faker library, an internal helper). They stay off the consumer's compile classpath and appear only at runtime, reducing classpath bleed and enabling better incremental compilation. The decision rule is identical to `java-library`: if it's in a signature/return type a consumer sees, use `Api`; otherwise `Implementation`. Over-using `Api` recreates the 'everything leaks' problem the variant split was designed to avoid.

code

kotlin · 6 lines
kotlin
// A custom assertion base class consumers extend
// class OrderAssert extends AbstractAssert<OrderAssert, Order> ...
dependencies {
    testFixturesApi("org.assertj:assertj-core:3.25.3")        // in signatures
    testFixturesImplementation("org.instancio:instancio-core:5.0.1") // internal only
}

go deeper

for a junior

Know both configurations exist and that one leaks to consumers and one doesn't.

for a middle

Apply the 'type in a consumer-visible signature → Api, otherwise Implementation' rule and explain the runtime-vs-compile classpath effect.

for a senior

Connect the split to incremental-compilation correctness and ABI boundaries, and spot over-broad Api usage in review.

for a principal

Set conventions that keep fixture APIs minimal so the org's test-support surface stays decoupled and fast to build.

## The api/implementation distinction, applied to fixtures Gradle's `java-library` plugin splits dependencies into **`api`** (part of your public ABI — leaks to consumers' compile classpath) and **`implementation`** (internal — only on your own compile/runtime classpath and consumers' *runtime* classpath). This shrinks compile classpaths, speeds incremental builds, and makes the abstraction boundary explicit. `java-test-fixtures` brings the same split to fixtures via **`testFixturesApi`** and **`testFixturesImplementation`**. ## Public surface of fixtures A fixtures module has a 'public API' too — the methods, types, and base classes that consuming tests call. If any of those signatures mention a third-party type, the consumer must be able to compile against it: ```kotlin dependencies { // AssertJ types appear in custom-assertion base classes consumers extend → API testFixturesApi("org.assertj:assertj-core:3.25.3") // Faker is used only inside builder bodies → internal testFixturesImplementation("com.github.javafaker:javafaker:1.0.2") } ``` Here a consumer that extends a custom-assertion base class needs AssertJ on its compile classpath, so AssertJ is `testFixturesApi`. Faker is never named in any signature the consumer touches, so it stays `testFixturesImplementation`. ## Why it matters - **Smaller compile classpaths** → faster, more correct incremental compilation; fewer accidental dependencies on internals. - **Honest API boundary** → consumers can't accidentally couple to a fixture's private helper library. - **Avoids version-leak surprises** → bumping an `implementation` lib doesn't recompile consumers. ## Decision checklist 1. Does a fixture method's parameter, return type, or extended base class reference the library's types? → `testFixturesApi`. 2. Is the library used only inside method bodies / private helpers? → `testFixturesImplementation`. 3. When unsure, default to `Implementation`; promote to `Api` only when compilation forces it. The anti-pattern is making everything `Api` 'to be safe' — that defeats the purpose and reintroduces the transitive-leak problems the split exists to prevent.

  • Your fixture builder returns a third-party `RandomGenerator` to callers but you declared that library as `testFixturesImplementation`. What happens?
    Consumers fail to compile because the return type isn't on their compile classpath. The type is part of the fixtures' public API, so the dependency must be `testFixturesApi`.
  • How does this split affect incremental build performance?
    `Implementation` dependencies stay off consumers' compile classpath, so bumping or changing them doesn't trigger recompilation of consuming modules — smaller, more stable compile classpaths mean faster, more correct incremental builds.

saying these in an interview costs you the question

  • Declaring every fixture dependency as `testFixturesApi` 'to be safe'.
  • Confusing the directions: thinking `Api` is the private one.

context