skip to content

What does JUnit 4's `@RunWith` annotation do, and what executes a test class that carries no `@RunWith` at all?

level: juniorimportance: must knowfreq 46%

answer

  1. Runner = getDescription() + run(RunNotifier)
  2. No @RunWith → JUnit4 extends BlockJUnit4ClassRunner
  3. Runner needs a Class<?> constructor
  4. Fresh instance per test method
  5. One runner per class — not repeatable

basics

~20 s

@RunWith(X.class) names the Runner class that takes over running that test class. Without it, JUnit falls back to its default runner — org.junit.runners.JUnit4, a subclass of BlockJUnit4ClassRunner — which finds the @Test methods, creates a new instance per method and applies @Before/@After and rules.

solid answer

~50 s

A `Runner` is the object that actually executes a test class: it reports a tree of `Description`s and fires events (`fireTestStarted`, `fireTestFailure`, `fireTestFinished`) at a `RunNotifier`, which is what IDEs and report writers listen to. `@RunWith(SomeRunner.class)` says "use this runner for this class instead of the default". The named class must extend `org.junit.runner.Runner` and expose a constructor taking `Class<?>` (or `Class<?>` plus a `RunnerBuilder` for suite-like runners). With no `@RunWith`, JUnit resolves a runner through a chain of builders — ignored class, `@RunWith`-annotated, legacy `suite()` method, JUnit 3 `TestCase`, and finally the JUnit 4 default — landing on `org.junit.runners.JUnit4`, a thin subclass of `BlockJUnit4ClassRunner`. That runner validates the class (one public no-arg constructor, public void no-arg `@Test` methods), builds one instance **per test method**, and wraps each method with `@Before`/`@After`, rules, `@Test(expected=…)` and `@Test(timeout=…)`. Write `@RunWith(JUnit4.class)` rather than `@RunWith(BlockJUnit4ClassRunner.class)` when you want the default explicitly — `JUnit4` is documented as the alias for whatever the current default is.

code

java · 11 lines
java
// No annotation: resolved to org.junit.runners.JUnit4
public class PriceTest {
    @Test public void addsTax() { /* ... */ }
}

// Same behaviour, stated explicitly. Prefer JUnit4.class over
// BlockJUnit4ClassRunner.class — JUnit4 is the alias for the current default.
@RunWith(JUnit4.class)
public class PriceTestExplicit {
    @Test public void addsTax() { /* ... */ }
}

go deeper

for a junior

Know that @RunWith picks the runner, that omitting it uses the default JUnit 4 runner, and that a fresh instance is created per test method.

for a middle

Describe the Runner contract (getDescription/run(RunNotifier)), what BlockJUnit4ClassRunner validates and builds, and the JUnit4 alias.

for a senior

Explain the builder chain that resolves a runner, the constructor requirements, and how the single-runner constraint shapes framework integrations.

for a principal

Discuss the class-level-strategy design and its consequence — one extension point per class — versus a composable extension model, and what that costs a large suite.

## What a runner is JUnit 4's execution model has one extension seam at the class level: the `Runner`. `org.junit.runner.Runner` is an abstract class with two responsibilities: ```java public abstract Description getDescription(); public abstract void run(RunNotifier notifier); ``` `Description` is an immutable tree describing what *will* run — a node per class and per method, carrying display names and annotations. IDEs draw their test tree from it before anything executes. `RunNotifier` is the event sink: the runner calls `fireTestStarted`, `fireTestFinished`, `fireTestFailure(Failure)`, `fireTestIgnored`, `fireTestAssumptionFailed` as it goes, and listeners (IDE panels, report writers) turn those into the output you see. Everything else — instantiating the class, ordering, filtering, applying annotations — is the runner's private business. ## What @RunWith does `@RunWith` is a class-level annotation whose single value is the runner class: ```java @RunWith(Parameterized.class) public class AdditionTest { … } ``` When JUnit builds a runner for a class it consults an annotated builder that looks for `@RunWith` (also on enclosing classes) and instantiates the named runner reflectively. The runner class must therefore offer a public constructor taking `Class<?>`, or one taking `Class<?>` and a `RunnerBuilder` when the runner needs to build runners for other classes — that second form is what `Suite` and `Categories` use. A missing or unusable constructor surfaces as an initialization error rather than a normal test failure. ## The default path With no annotation, JUnit's `AllDefaultPossibilitiesBuilder` tries a fixed sequence of builders and takes the first that produces a runner: the ignored-class builder (a class annotated `@Ignore`), the annotated builder (`@RunWith`), the suite-method builder (a legacy static `suite()` method), the JUnit 3 builder (a subclass of `junit.framework.TestCase`), and finally the JUnit 4 builder — which returns `org.junit.runners.JUnit4`. `JUnit4` is `final` and extends `BlockJUnit4ClassRunner`; its javadoc explains it exists as a stable alias so that "if future versions of JUnit change the default Runner class, they will also change the definition of this class". Hence the advice to annotate with `@RunWith(JUnit4.class)` when you want the default explicitly, never with `@RunWith(BlockJUnit4ClassRunner.class)`. ## What BlockJUnit4ClassRunner actually does 1. **Validates** the class and fails fast with an initialization error if, say, there is no public no-arg constructor, a `@Test` method is not `public void` with no parameters, or a `@Rule` field is private or static. 2. **Collects children** — the `@Test` methods, honouring `@Ignore` and `@FixMethodOrder`. 3. **For each method**: constructs a *fresh instance* of the test class (test isolation by construction), builds a `Statement` for the method invocation, then wraps it with `@Test(expected=…)`, `@Test(timeout=…)`, the `@Before` methods, the `@After` methods, and finally the `@Rule`s. 4. **Notifies** the `RunNotifier` around each child, so failures land against the right `Description`. The class-level statement adds `@BeforeClass`, `@AfterClass` and `@ClassRule`. ## The one hard limitation `@RunWith` takes exactly one runner and is not repeatable, and the runner owns the whole lifecycle — so a class can never have two runners. That constraint is the source of most real-world friction (wanting a parameterized class that also boots a framework context) and the reason so many frameworks ship rule-based alternatives to their runners. ## Runners you will meet `JUnit4` (default), `Suite` (aggregate other classes), `Parameterized` (cross-product of methods and data rows), `Categories` (a filtered suite), `Enclosed` (run nested classes), plus framework runners from Spring, Mockito and others. All are just classes implementing the same two-method contract. ## Interview framing The crisp answer: *`@RunWith` swaps the class-level execution strategy; the default is `JUnit4`, a `BlockJUnit4ClassRunner` that creates one instance per test method and applies the annotation lifecycle; a runner's job is to publish `Description`s and fire events at a `RunNotifier`.*

  • What constructor must a class named in `@RunWith` provide?
    A public constructor taking `Class<?>` — the test class being run — or, for runners that build runners for other classes, one taking `Class<?>` and a `RunnerBuilder`. JUnit instantiates the runner reflectively, so a missing or private constructor produces an initialization error attributed to the test class rather than a normal failure.
  • Why does the default runner create a new instance of the test class for every test method?
    To give tests isolation by construction: instance fields cannot leak from one test to the next, so tests do not depend on execution order. It is also why `@Rule` fields are non-static and get re-created per test. Anything you genuinely want shared has to be explicitly static — `@BeforeClass` or `@ClassRule`.

saying these in an interview costs you the question

  • Thinking a class without `@RunWith` is not run at all — the default runner is selected automatically.
  • Writing `@RunWith(BlockJUnit4ClassRunner.class)` instead of `@RunWith(JUnit4.class)` when stating the default explicitly.
  • Believing one instance of the test class is reused for all its methods.
  • Claiming you can stack two `@RunWith` annotations or list two runners — the annotation takes exactly one and is not repeatable.
  • Confusing the runner with the assertion library or the build's test task; the runner is purely the in-JVM execution strategy for one class.

context