In JUnit 4, what is a `@Rule`, and what can it do that plain `@Before` and `@After` methods cannot?
answer
- Statement.evaluate() — rule wraps the call
- public, non-static field, @Rule
- apply(base, Description) returns new Statement
- before + after + around (catch/retry/skip/timeout)
- composition, not a base class
basics
~20 sA @Rule is a public, non-static field holding an object that wraps each test method. Because it wraps the call, it can run code before and after the test and also catch failures, retry, time out or skip it. @Before/@After only run beside the test.
solid answer
~50 sJUnit 4 executes a test method as a `Statement` — an object with `evaluate()`. A rule implements `TestRule.apply(Statement base, Description description)` and returns a **new** statement that typically does setup, calls `base.evaluate()` inside a `try`, and tears down in `finally`. You declare it as a **public, non-static field** annotated `@Rule` (a public non-static method returning the rule also works); the runner validates this and fails the class otherwise. The statement handed to the rule already contains the `@Before` methods, the test method and the `@After` methods, so rule code sits outside all of them. The payoff is control plus reuse. Because the rule owns the call to `base.evaluate()`, it can swallow or translate the exception, retry, apply a timeout on another thread, or throw `AssumptionViolatedException` to skip the test. And it is a *class*, so it composes into any test class — no shared base class, and you can stack several rules.
code
java · 12 linespublic class ReportWriterTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void writesReportFile() throws IOException {
File out = folder.newFile("report.csv");
new ReportWriter().writeTo(out);
assertTrue(out.length() > 0);
}
}go deeper
Know the declaration (public non-static field, @Rule) and name a built-in such as TemporaryFolder, plus the one-line difference from @Before/@After.
Explain the Statement/apply mechanism, that the wrapped statement includes @Before/@After, and give a concrete capability like retry or timeout that methods cannot provide.
Discuss when to build a rule versus a base class, ordering between multiple rules, stack-trace noise, and the risk of a rule that hides failures.
Frame rules as the JUnit 4 extension point and the composition-over-inheritance answer for shared test infrastructure, and weigh the cost of implicit setup on test readability.
## The statement model JUnit 4 does not invoke your test method straight from the runner. It builds a `Statement` — an abstract class with one method, `void evaluate() throws Throwable` — which, when invoked, runs the whole method-level lifecycle. A rule is an object that is handed that statement and returns a statement to run *in its place*. The modern interface is `org.junit.rules.TestRule`: ```java Statement apply(Statement base, Description description); ``` `base` is what JUnit would otherwise have run; `Description` is read-only metadata about the test (class name, method name, annotations). The idiomatic implementation returns an anonymous `Statement` that does setup, calls `base.evaluate()` in a `try`, and cleans up in `finally`. Rules are therefore *decorators* around the test. ## How you declare one The field must be **public**, **non-static**, and of a type implementing `TestRule` (preferred) or the older `MethodRule`. Annotate it `@Rule`. A public, non-static *method* that returns a rule may also carry `@Rule`. The runner collects these reflectively and validates them: a private field or a static field annotated `@Rule` is an initialization error, so the class fails before a single test runs. Crucially, the statement JUnit passes to a `@Rule` already contains the class's `@Before` methods, the test method and the `@After` methods. Rule code therefore runs strictly outside all of them — a rule's setup happens before every `@Before`, and its cleanup after every `@After`. ## What rules can do that @Before/@After cannot 1. **See and change the outcome.** `@After` runs after the test but cannot observe the exception. A rule catches whatever `base.evaluate()` throws, so it can log the failure, translate it into a better message, add extra failures, or swallow it and let the test pass. 2. **Retry.** Call `base.evaluate()` in a loop until it succeeds or a retry budget is exhausted. 3. **Skip.** Throw `AssumptionViolatedException` before calling `base` and the test is reported as skipped, not failed — a decision `@Before` can make only by throwing the same exception, but a rule can make it for every class that uses it. 4. **Bound execution time.** The built-in `Timeout` rule runs the base statement on another thread and fails the test if it overruns. Nothing method-shaped can do that. 5. **Guarantee cleanup by construction.** The `finally` lives inside the rule, so no test class can forget it. 6. **Compose instead of inherit.** A rule is a reusable class. `@Before` logic is shared by putting it in a base class, and Java gives you exactly one superclass; a test class can hold as many rule fields as it likes. ## Two flavours of instance and class scope `@Rule` fields are per-instance, and JUnit creates a fresh test instance for every test method, so each test gets a fresh rule object and rules re-run per method. `@ClassRule` is the static sibling: a public static `TestRule` field whose statement wraps `@BeforeClass`, the entire class body, and `@AfterClass`, running once. ## The rules that ship with JUnit `TemporaryFolder` (fresh directory per test, deleted afterwards), `ExternalResource` (base class with `before()`/`after()` for any resource), `Timeout`, `TestName` (current method name), `ErrorCollector` (accumulate several failures), `TestWatcher` (callbacks for succeeded/failed/skipped/finished), `Verifier`, `Stopwatch`, `DisableOnDebug`. Most custom rules extend `ExternalResource` or `TestWatcher` rather than implementing `TestRule` from scratch. ## Pitfalls The rule field is initialised per test instance, so do not put expensive state in a `@Rule` if it can be a `@ClassRule`. Rules add frames to stack traces, which can obscure the real failure point. Order between multiple rule fields is not the source order — it depends on reflection unless you pin it with `@Rule(order = …)` (JUnit 4.13) or a `RuleChain`. And a rule that quietly swallows exceptions is a debugging trap: if it catches, it should re-throw or report. ## Why interviewers ask Rules are the JUnit 4 extension point. Understanding that a rule *wraps* rather than *precedes* the test is what separates a candidate who copy-pastes `TemporaryFolder` from one who can build shared test infrastructure.
- Why must a `@Rule` field be public and non-static?The runner reads rule fields reflectively through its `TestClass` model and validates them before running anything, and it requires public access rather than forcing accessibility. Non-static is required because a `@Rule` is per-test-instance: JUnit creates a new instance for every test method, so each test gets its own rule object. A static field annotated `@Rule` is an initialization error — that is `@ClassRule`'s job.
- Can a rule tell whether the test passed or failed?Yes. Since the rule calls `base.evaluate()` itself, it sees the `Throwable` if one is thrown and can act on it. The built-in `TestWatcher` packages exactly this: override `succeeded`, `failed`, `skipped` or `finished` and it invokes the right callback based on what came out of the wrapped statement — the usual way to dump diagnostics on failure.
@Before/@After are the doorman who greets you and tidies up after you leave; a rule is the whole building around the room — it can lock the door, restart the visit, or throw you out early.
saying these in an interview costs you the question
- Saying a `@Rule` runs once per class — that is `@ClassRule`; a `@Rule` runs around every test method.
- Declaring the rule field private (or static) and expecting it to work — the runner rejects the class with a validation error.
- Claiming a rule cannot influence the result; a rule can swallow, translate or add failures because it owns the call to `base.evaluate()`.
- Thinking `@RunWith` is required for rules to work — the default runner already applies them.
- Assuming rule code runs between `@Before` and the test; it runs outside `@Before` and `@After` entirely.