Across a large JUnit 4 suite, dozens of test classes need the same expensive setup — a database container, a seeded workspace, a fixed clock. How do you weigh implementing that as a shared rule, an abstract base test class, or a static singleton, and what are the failure modes of each?
answer
- One superclass — rules compose, base classes don't
- Singleton = cheapest start, no teardown, no reset
- ExternalResource before()/after() = guaranteed cleanup
- Rule fronting a lazy singleton = one start + visibility
- Infrastructure in rules, domain data in builders
basics
~20 sRules compose (a class can hold many), a base class does not (Java has one superclass) and hides setup in an invisible parent. Singletons give suite-wide reuse but no teardown or isolation. The usual answer is a rule that fronts a lazily started singleton, with a per-test rule restoring isolation.
solid answer
~50 sI compare three axes: **composability**, **lifecycle control**, and **visibility**. - **Base class** — costs the single inheritance slot, cannot be combined with another base, and hides setup from the reader. Fine for one narrow family of tests, bad as a platform. - **Static singleton** — the resource starts once for the JVM, which is the cheapest option, but there is no framework-managed teardown, no per-test reset, and no ordering guarantee. - **Rule** — composes freely (several rule fields per class), is explicit at the top of the test, and gets guaranteed cleanup via `ExternalResource`. In practice I combine them: a `@ClassRule`, or a rule that ensures a lazily created static singleton is started once and never stopped per class, plus a per-test `@Rule` that restores isolation (truncate, roll back, reset the clock). I pin ordering with `@Rule(order = …)` or a `RuleChain` exposed from a fixtures factory, and I keep domain data out of rules — that belongs in explicit builders, or tests become unreadable.
code
java · 25 linespublic final class DatabaseRule extends ExternalResource {
private static volatile Database instance;
private static Database started() {
if (instance == null) {
synchronized (DatabaseRule.class) {
if (instance == null) {
Database db = Database.start();
Runtime.getRuntime().addShutdownHook(new Thread(db::stop));
instance = db;
}
}
}
return instance;
}
private Database db;
@Override protected void before() { db = started(); }
@Override protected void after() { db.truncateAll(); } // isolation, not shutdown
public Database database() { return db; }
}go deeper
Recognise that repeated setup should be shared and that a rule is the JUnit 4 way to do it without copying code.
Contrast rule versus base class on composability and visibility, and pick @Rule versus @ClassRule by cost and isolation needs.
Design the composite: shared resource fronted by a rule, per-test reset, explicit ordering, and clear failure attribution when setup breaks.
Treat it as a platform decision — cost curve, isolation budget, parallelism strategy, readability of tests, and the convention the whole organisation will follow.
## Framing the decision The question is really about the cost curve of shared test infrastructure: startup cost versus isolation, and implicit setup versus readable tests. Three mechanisms are on the table. ### Abstract base test class Setup lives in `@Before`/`@BeforeClass` on a parent. It works, and it is what most suites drift into. Failure modes: Java allows one superclass, so the first shared concern claims the slot and the second cannot be added; hierarchies grow three deep and nobody knows what runs; a reader of the test file sees no evidence of the database at all; and turning a piece of setup off for one class means overriding a hook or adding a flag to the parent. It also couples every test to a class that must keep changing. ### Static singleton A `static { }` block or a lazily initialised holder starts the container the first time any test touches it, and it lives for the JVM. Strengths: the cheapest possible model — one start for the whole suite regardless of how many classes use it, which a per-class `@ClassRule` cannot match. Failure modes: there is no framework hook to stop it (people rely on a shutdown hook or on the JVM exiting), no per-test reset so state accumulates, no ordering relative to other setup, and initialisation failures surface as opaque `ExceptionInInitializerError` far from the cause. Under parallel execution the lazy initialisation must be thread-safe. ### Rule A `TestRule` — usually extending `ExternalResource` — declared as a field. Strengths: composition (a class can hold as many rules as it needs, and they nest), explicitness (the field is visible in the test class), guaranteed cleanup in a `finally`, choice of scope via `@Rule` versus `@ClassRule`, and reusability across unrelated classes without inheritance. Failure modes: per-class scope still restarts the resource for every class unless it fronts something shared; ordering between rules is undefined until pinned; rules add stack frames that obscure failures; and a rule that swallows exceptions hides real defects. ## The composite answer The pattern that survives contact with a large suite is a rule whose `before()` *ensures* a lazily created, suite-scoped singleton is running, and whose `after()` does **not** stop it — cleanup happens once at JVM exit. The rule then gives you the visibility and composability of a rule with the one-start cost of a singleton. A second, per-test rule restores isolation: open a transaction and roll it back, truncate the tables, reset the fake clock, clear the cache. Where a suite class exists, a `@ClassRule` on the suite is the cleanest expression: the resource starts before the first contained class and stops after the last. The weakness is that it only helps when tests are actually run through that suite — an engineer running one class in an IDE takes a different path, so the rule must still work standalone. ## Isolation is the real budget Every step toward sharing buys speed with isolation. Ask, per resource: what state can one test leave behind that another can observe? Shared containers leak rows, sequences, caches, clocks and files. Decide the reset mechanism *before* choosing the scope; if there is no cheap reset, per-class or per-test scope is the honest choice even though it costs time. ## Parallelism Once tests run in parallel, a `@ClassRule` field and any singleton are shared mutable state across threads. Per-method rules are per-instance and therefore naturally isolated, which is an argument for keeping the per-test reset in a `@Rule` even when the heavy resource is shared. Anything shared needs either thread-safety or per-thread partitioning (separate schemas, separate directories, separate ports). ## Readability and the hidden-dependency trap Rules make setup implicit. That is a feature for process-level resources (a container, a temp directory, a timeout) and a bug for domain data. A rule that quietly inserts "a customer with three orders" makes every test in the class depend on data the test body never mentions, and the first person to change that fixture breaks twenty tests. Rules for infrastructure; explicit builders inside the test for data. ## Diagnostics and failure attribution When shared setup fails, attribution matters. A class-rule failure is reported against the class, not any test, and reads as "tests disappeared". Make setup failures loud and specific, and consider a `TestWatcher` placed outermost so it still observes and reports failures thrown by inner rules' setup. ## What a strong answer sounds like Name the three options, give each a concrete failure mode, land on the composite (shared singleton fronted by a rule, isolation restored per test), and mention the second-order concerns: ordering, parallelism, failure attribution, and keeping domain data out of the infrastructure layer.
- When is an abstract base test class still the right call?When the shared behaviour is genuinely intrinsic to one family of tests and there is no second concern competing for the inheritance slot — for example a small set of contract tests that all exercise the same abstract interface. Even then, prefer putting the mechanics in a rule and letting the base class only declare the rule field, so the behaviour stays reusable outside the hierarchy.
- How does this change once tests run in parallel?Anything reached through a static field or a `@ClassRule` becomes shared mutable state across threads, so it needs thread safety or partitioning — separate schemas, directories or ports per thread. Per-method `@Rule` objects are created per test instance and are naturally isolated, so the per-test reset should stay there. Lazy singleton initialisation must be correctly published, and teardown becomes a JVM-exit concern rather than a per-class one.
A base class is furniture bolted to the floor of one room; a rule is a plug-in appliance you can carry into any room and stack with others.
saying these in an interview costs you the question
- Defaulting to an abstract base test class without acknowledging that it consumes the single inheritance slot.
- Assuming a `@ClassRule` means the resource starts once for the whole suite — it starts once per class unless it fronts something shared or lives on a suite class.
- Putting domain fixture data in a rule, making every test depend on state its body never names.
- Ignoring reset: sharing a resource without a per-test cleanup mechanism converts speed into flakiness.
- Not considering parallel execution, where shared rule state and singletons become concurrency problems.