A JUnit 4 test class is already annotated `@RunWith(Parameterized.class)`, but you also need a second framework's runner — say one that boots an application context, or one that initialises annotated mock fields. What constraint do you hit, and what are the ways around it?
answer
- @RunWith holds one runner, not repeatable
- Runner owns the whole lifecycle — no co-ownership
- Rules compose; runners don't
- UseParametersRunnerFactory = supported hook under Parameterized
- Symptom: null injected fields after deleting a runner
basics
~20 s@RunWith takes exactly one runner and is not repeatable, and the runner owns the whole class lifecycle — so a class cannot have two. Work around it by replacing one runner with its rule equivalent (rules compose), by supplying a Parameterized runner factory, or by splitting the class.
solid answer
~60 sThe constraint is structural: `@RunWith` holds a single runner class, is not repeatable, and the runner owns everything — instantiating the class, invoking methods, notifying listeners. Two runners cannot both own that, so there is no annotation-level composition in JUnit 4. The practical ways out, in order of preference: 1. **Swap a runner for its rule equivalent.** Most frameworks that ship a runner also ship rules that do the same work — typically a class rule plus a method rule. Rules compose without limit, so the class keeps `@RunWith(Parameterized.class)` and gains the other framework through rule fields. 2. **`@Parameterized.UseParametersRunnerFactory(MyFactory.class)`** — `Parameterized` delegates each parameter set to a child runner produced by a factory. A framework can ship a factory whose runner extends `BlockJUnit4ClassRunnerWithParameters`, which is the supported integration point. 3. **Write a delegating runner** that wraps another — real work and brittle against upstream changes. 4. **Split the class** so each concern gets its own runner. This single-seam design is the limitation JUnit 5's composable extensions were created to remove.
code
java · 17 lines@RunWith(Parameterized.class)
public class TaxCalculatorTest {
@ClassRule
public static ExternalResource context = new ContextRule(); // once per class
@Rule
public MethodRule injection = new InjectionRule(this); // per test instance
@Parameters(name = "{index}: {0}% on {1}")
public static Iterable<Object[]> data() { /* ... */ }
@Parameter(0) public int rate;
@Parameter(1) public BigDecimal amount;
@Test public void appliesRate() { /* ... */ }
}go deeper
Know that a class can have only one @RunWith and that rules are the way to add extra behaviour alongside it.
Explain why ownership prevents composition, and apply the rule-equivalent workaround correctly (class rule plus method rule).
Compare the options — rules, a parameterized runner factory, a delegating runner, splitting the class — and diagnose the silent NPE symptom in the wild.
Draw the design lesson: a single total-ownership seam forces framework runners to become monoliths, which is what composable extension models were introduced to fix.
## Why only one runner `@RunWith` is declared with a single `Class<? extends Runner>` value and is not repeatable. More fundamentally, the `Runner` contract is total: given the test class, the runner decides what the tests are, constructs instances, invokes methods, applies annotations and fires every notification. Two objects cannot both be the sole owner of that process. JUnit 4 therefore has exactly **one class-level extension seam**, and whoever claims it excludes everyone else. This is the root cause of a very common frustration: a data-driven class needs `Parameterized`, but the same class also needs a framework's context bootstrapping or field initialisation, and each of those historically arrived as a runner. ## Way out 1 — rules instead of a runner (usually correct) Rules are the composable seam. A class may declare any number of `@Rule` and `@ClassRule` fields, and they nest deterministically once ordered. Frameworks that started with a runner almost always added rule equivalents precisely because of this constraint — typically a **class rule** for the once-per-class work (starting or caching a context) and a **method rule** for the per-test work (injecting into the instance, resetting state). Anything that must write fields on the test instance has to be a `MethodRule`, since only that interface receives the instance. So the pattern is: keep the runner you cannot replace (`Parameterized`, `Suite`, `Enclosed`), and express the other concern as rule fields. ## Way out 2 — a parameterized runner factory `Parameterized` does not run methods itself: it builds one child runner per parameter set. `@Parameterized.UseParametersRunnerFactory(MyFactory.class)` replaces the factory that creates those children; the default produces a `BlockJUnit4ClassRunnerWithParameters`. A framework can therefore ship a factory whose runner adds its behaviour on top of the parameterized child runner, and you get both without touching `@RunWith`. This is the supported extension point when the other framework's behaviour genuinely needs runner-level control. ## Way out 3 — a delegating runner You can write a `Runner` that constructs another runner and forwards `getDescription()` and `run(RunNotifier)` to it, inserting behaviour around the call. It works for coarse concerns (start something before, stop it after) but not for anything needing to influence instantiation or per-method statements, and it binds you to another framework's internals. Treat it as a last resort. ## Way out 4 — split the class Often the cleanest response. If a class wants to be parameterized *and* to boot a heavyweight context, ask whether the parameterized part is really a pure unit calculation that needs no context at all. Splitting removes the conflict and usually improves the test. ## Diagnosing the symptom The compiler stops you from writing two `@RunWith` annotations — "duplicate annotation". The subtler failure is when someone deletes one of them to make it compile: the framework whose runner was removed silently stops doing its work, so mock fields stay null or the context is never injected, and the test fails with a `NullPointerException` that looks unrelated. When you see NPEs on framework-injected fields in a class that also carries `Parameterized` or `Suite`, check what happened to the runner. ## Second-order consequences Because there is one seam, framework runners tend to become monoliths: a single runner ends up owning context caching, dependency injection, transactions and more, because it cannot delegate to a peer. That is expensive to maintain and impossible to opt out of piecemeal — the argument that produced the composable extension model in JUnit 5, where any number of extensions can register lifecycle callbacks for the same class. ## How to answer Name the constraint precisely (single, non-repeatable annotation; runner owns the lifecycle), give the rule-based workaround first, mention the parameterized runner factory as the supported runner-level hook, note delegation and splitting, and close with the design observation that this is exactly the limitation composable extensions removed.
- Why can rules be combined freely when runners cannot?A rule only decorates a `Statement`: it receives the statement JUnit was going to run and returns another one, so rules nest arbitrarily deep and each is unaware of the others. A runner instead *owns* discovery, instantiation, invocation and notification for the class — a total responsibility that cannot be shared. Composition works when the contract is a decorator; it fails when the contract is ownership.
- What does `@Parameterized.UseParametersRunnerFactory` change?`Parameterized` creates one child runner per parameter set through a factory, by default producing a `BlockJUnit4ClassRunnerWithParameters`. The annotation substitutes your own factory, so another framework can inject runner-level behaviour beneath `Parameterized` without competing for the class's single `@RunWith` slot. It is the sanctioned integration point for that combination.
saying these in an interview costs you the question
- Claiming you can list two runners in `@RunWith` or repeat the annotation — the compiler rejects both.
- Suggesting a base class with a different `@RunWith` will combine the two; the runner is resolved once for the class actually being run.
- Deleting one runner to make the class compile and not noticing that its framework silently stopped initialising fields.
- Reaching straight for a custom delegating runner before checking whether the framework ships rule equivalents.
- Describing this as a bug rather than a consequence of the runner owning the whole class lifecycle.