A JUnit Rule is a reusable object, marked with @Rule, that wraps each test method. You implement TestRule.apply, put setup before base.evaluate(), and cleanup in a finally after it. That is the same before/after job as setUp and tearDown, but you write the logic once and drop the field into any test class. Built-in rules cover temp files, timeouts, and expected exceptions. In JUnit 5 and 6 the same idea is an Extension (@ExtendWith or @RegisterExtension). Keep Rules if you still run JUnit 4 or Vintage. Use Extensions for new tests.
Key Takeaways
- setUp and tearDown live inside one test class. A Rule lives in its own class, so every test that needs a clean database or a fake server can share it.
- A TestRule wraps the test Statement. Code before evaluate() runs before the test. A finally block after evaluate() still runs if the test throws.
- The @Rule field must be public and not static. Use @ClassRule on a public static field when the resource should start once for the whole class.
- Prefer ExternalResource when you only need before/after for a file, socket, or database. Implement TestRule yourself when you need the test Description or extra methods on the rule.
- Rule order is not something you should leave to luck. Use RuleChain or @Rule(order = ...) so outer and inner wrappers stay predictable.
- JUnit 5 and 6 replaced Rules with Extensions. TemporaryFolder becomes @TempDir, ExpectedException becomes assertThrows, and a custom DatabaseResetRule becomes BeforeEachCallback.
You write setUp to wipe a database. It works. Then you write a second test class that talks to the same database, so you copy setUp. Then a third class. Six months later the reset logic changes and you miss one copy. Tests start failing in ways that look like product bugs.
That is the problem JUnit Rules were built to solve. A Rule is the same before/after idea as setUp and tearDown, lifted out of the test class so you can reuse it. This post keeps the original database example, shows how the wrapper actually runs, covers the rules JUnit 4 already ships, and maps each one to the JUnit 5 Extension you should use on new code. The @Rule marker itself is just a Java annotation. The runner reads it with reflection the same way it finds @Test.
Glossary terms in this post
- JUnit Rule A JUnit Rule is a reusable object that wraps a JUnit 4 test method (or a whole class with @ClassRule) so setup,...
- JUnit Extension A JUnit Extension is the Jupiter replacement for a JUnit 4 Rule. You implement callbacks such as BeforeEachCallback or AfterAllCallback, then register...
- Java Annotation A Java annotation is metadata attached to source code with an @Name tag. You declare a custom annotation with @interface, then control...
- Java Virtual Machine The Java Virtual Machine, or JVM, is the runtime engine that executes Java bytecode. When you compile Java source with javac you...
What a JUnit Rule Actually Is
In JUnit 4, @Before and @After (often still named setUp and tearDown from JUnit 3) run around each test method. They work. They also belong to that one class.
A Rule is an object the runner wraps around the test. You implement TestRule, JUnit hands you a Statement that means “run the test,” and you return a new Statement that does extra work around it. From the TestRule javadoc: a rule can add checks, do setup and cleanup, or just watch the test. It can do everything @Before / @After / @BeforeClass / @AfterClass can do, and it is easier to share.
The runner applies rules around @Before, the @Test method, and @After. So the order for one method looks like this:
flowchart TD
R["fa:fa-layer-group <b>@Rule</b><br/>outer wrapper"] --> B["fa:fa-play <b>@Before</b><br/>setUp"]
B --> T["fa:fa-check <b>@Test</b>"]
T --> A["fa:fa-stop <b>@After</b><br/>tearDown"]
A --> C["fa:fa-layer-group <b>@Rule</b><br/>cleanup / finally"]
classDef rule fill:#dbeafe,stroke:#1d4ed8,stroke-width:2px,color:#0f172a
classDef hook fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#0f172a
classDef test fill:#c8e6c9,stroke:#16a34a,stroke-width:2px,color:#0f172a
class R,C rule
class B,A hook
class T test
Popular reads
View All
That wrapping is why a Rule can enforce a timeout on the whole method, including setUp, or wipe a database even if setUp in the test class does something else.
MethodRule is the older interface. TestRule replaced it in JUnit 4.9. New rules should implement TestRule.
Why setUp Does Not Scale
Imagine you test a UserService that writes to a real database in instrumentation tests. You want every method to start from an empty schema. In one class, @Before is fine:
1
2
3
4
@Before
public void setUp() {
getTargetContext().deleteDatabase(DatabaseHelper.DB_NAME);
}
That is exactly the pattern in the Android database testing post. The pain starts when OrderServiceTest, ReportServiceTest, and SyncWorkerTest need the same wipe. You either copy the method, push it into a base class that every test must extend, or extract a Rule.
A base class looks tidy until you need two independent helpers (database reset and a mock server) and Java will not let you extend both. A Rule is composition. A test class can hold as many @Rule fields as it wants. That is the same reason the Template Method pattern shows up in test frameworks, and also why composition with Rules (or Extensions) ages better than a deep test superclass.
A Custom Rule: Reset the Database
Here is the original example, with two fixes that matter in real suites. The field is public (JUnit 4 ignores a package-private @Rule). Cleanup, if you add it, sits in finally so a failed assertion still leaves the next test a clean database.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public class DatabaseResetRule implements TestRule {
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
clearDatabase();
try {
base.evaluate();
} finally {
// optional: close connections, delete files
}
}
};
}
private void clearDatabase() {
// drop tables, delete the file, or run a truncate script
}
}
Description is there if you need the test method name or the class. You do not have to use it.
Use the rule in every test class that needs a clean store:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.junit.Rule;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
public class UserServiceTest {
@Rule
public DatabaseResetRule db = new DatabaseResetRule();
@Test
public void shouldAddUserToDatabase() {
UserService service = new UserService();
service.add(new User("Ajit"));
assertNotNull(service.getUserByName("Ajit"));
}
@Test
public void shouldGetAllUsers() {
UserService service = new UserService();
service.add(new User("Ajit"));
assertThat(service.getAllUsers().size(), is(1));
}
}
If the only thing you need is start/stop of a resource, skip the raw Statement and extend ExternalResource. That is what TemporaryFolder does internally:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.junit.rules.ExternalResource;
public class DatabaseResetRule extends ExternalResource {
@Override
protected void before() {
clearDatabase();
}
@Override
protected void after() {
// close the helper, delete the file
}
private void clearDatabase() {
// ...
}
}
Same @Rule field in the test. Less wrapping code to get wrong.
flowchart LR
T1["fa:fa-file UserServiceTest"] --> R["fa:fa-database <b>DatabaseResetRule</b>"]
T2["fa:fa-file OrderServiceTest"] --> R
T3["fa:fa-file ReportServiceTest"] --> R
R --> D["fa:fa-hdd empty database"]
classDef test fill:#dbeafe,stroke:#1d4ed8,stroke-width:2px,color:#0f172a
classDef rule fill:#c8e6c9,stroke:#16a34a,stroke-width:2px,color:#0f172a
classDef db fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#0f172a
class T1,T2,T3 test
class R rule
class D db
More from Java
View AllChange clearDatabase once. Every class that holds the rule picks it up. That is the whole benefit over copying setUp.
Rules JUnit 4 Already Ships
You do not have to write a Rule for every job. The JUnit 4 Rules wiki lists the ones in org.junit.rules.
TemporaryFolder creates files and directories and deletes them when the method ends. From 4.13 you can fail the test if deletion fails:
1
2
3
4
5
6
7
8
@Rule
public TemporaryFolder folder = TemporaryFolder.builder().assureDeletion().build();
@Test
public void writesAFile() throws IOException {
File input = folder.newFile("input.txt");
// ...
}
ExpectedException lets you assert type and message inside the method, which is more precise than @Test(expected = ...).
1
2
3
4
5
6
7
8
9
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void rejectsANullIcon() {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Icon is null");
new DigitalAssetManager(null, null);
}
Timeout applies one limit to every method in the class, including setUp. Pair it with DisableOnDebug if you do not want breakpoints to fail the test.
TestName exposes the current method name, useful in logs or in a per-test output folder.
ErrorCollector keeps going after the first failure so you see every bad row in a table, not just the first.
TestWatcher hooks succeeded, failed, skipped, starting, and finished without changing the test result. Good for extra logging.
Verifier is the base for “the test passed, but the log is empty, so fail anyway.”
@ClassRule is the class-level twin. The field must be public static. Use it to connect a server once for a suite, not once per method.
RuleChain (or @Rule(order = ...) since 4.13) fixes wrapper order. Without it, multiple fields are applied in an order that depends on the JVM reflection API, which is not a contract you want.
1
2
3
4
5
@Rule
public TestRule chain = RuleChain
.outerRule(new LoggingRule("outer"))
.around(new DatabaseResetRule())
.around(new LoggingRule("inner"));
Outer starts first and finishes last, like nested try/finally.
Android tests used the same API. ActivityTestRule in older Espresso suites is a Rule that launches an Activity before the method and finishes it after. See the ListView instrumentation and Espresso posts. Newer AndroidX tests move to ActivityScenario instead of that Rule.
JUnit 5 and 6: Extensions Do This Job Now
If you are starting a project on current JUnit (the docs call the stack JUnit Platform + Jupiter, version 6.x on the user guide), do not add new TestRule classes. Jupiter replaced Rules with Extensions. Baeldung’s JUnit 5 extensions guide is a solid walkthrough. JUnit Vintage can still run old @Rule tests on the platform, but Vintage is deprecated there and is meant as a bridge.
The database reset becomes a callback:
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
public class DatabaseResetExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
clearDatabase();
}
private void clearDatabase() {
// same wipe as the Rule
}
}
Register it on the class:
1
2
3
4
5
6
7
8
9
10
11
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(DatabaseResetExtension.class)
class UserServiceTest {
@Test
void shouldAddUserToDatabase() {
// ...
}
}
Or keep an instance, the closest match to a @Rule field:
1
2
3
4
5
6
import org.junit.jupiter.api.extension.RegisterExtension;
class UserServiceTest {
@RegisterExtension
DatabaseResetExtension db = new DatabaseResetExtension();
}
Built-in mappings you will use constantly:
| JUnit 4 Rule | JUnit 5 / 6 |
|---|---|
| TemporaryFolder | @TempDir on a field or parameter |
| ExpectedException | Assertions.assertThrows |
| Timeout | @Timeout on a method or class |
| TestName | TestInfo parameter on the test method |
| @ClassRule | BeforeAllCallback / AfterAllCallback |
| custom TestRule | BeforeEachCallback, AfterEachCallback, or a full Extension |
flowchart TD
Q{"Shared test lifecycle?"}
Q -->|JUnit 4 or Vintage| R["fa:fa-link <b>TestRule + @Rule</b>"]
Q -->|JUnit 5 or 6 Jupiter| E["fa:fa-puzzle-piece <b>Extension</b><br/>@ExtendWith / @RegisterExtension"]
Q -->|Only this one class, three lines| S["fa:fa-code <b>@Before / @BeforeEach</b>"]
classDef q fill:#dbeafe,stroke:#1d4ed8,stroke-width:2px,color:#0f172a
classDef old fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#0f172a
classDef neu fill:#c8e6c9,stroke:#16a34a,stroke-width:2px,color:#0f172a
classDef simple fill:#e0f2fe,stroke:#0891b2,stroke-width:2px,color:#0f172a
class Q q
class R old
class E neu
class S simple
Spring Boot tests follow the same split. Older suites use Spring’s JUnit 4 SpringClassRule and SpringMethodRule. Current Spring Boot tests use @ExtendWith(SpringExtension.class) or @SpringBootTest, which registers that extension for you. Mockito’s @ExtendWith(MockitoExtension.class) replaced MockitoJUnit.rule().
Mistakes That Make the Rule Look Broken
The field is not public. JUnit 4 will not apply it. No error, just no reset. Make it public (or use a public getter method annotated with @Rule).
You forgot @Test. Rules wrap test methods. A public method without @Test is not a test.
Cleanup is not in finally. If base.evaluate() throws, code after it never runs. The next test inherits dirty state. ExternalResource already uses the safe pattern.
You needed @ClassRule but used @Rule. Starting Docker or a database per method is slow and flaky. One static @ClassRule (or BeforeAllCallback) is the right scope. Wiping rows can still be a per-method Rule inside that.
Several @Rule fields and failures that depend on order. Order is undefined unless you set order or RuleChain. If one rule starts a server and another needs the port, make that nesting explicit.
You added a Rule on JUnit 5 by habit. Jupiter ignores org.junit.Rule. The test compiles if Vintage and Jupiter both sit on the classpath, and then you wonder which engine ran. Pick one model per test class.
Wrapping Up
setUp and tearDown are the right tool when the logic is local. The moment the same wipe, server, or temp directory shows up in a second class, pull it into a JUnit Rule. Implement TestRule or extend ExternalResource, put a public @Rule field on each test, and keep cleanup in finally.
That design is still how a lot of JUnit 4 and Android instrumentation suites work. On JUnit 5 and 6 the same design is an Extension. The names changed. The lesson did not: shared lifecycle belongs in a reusable wrapper, not in a copied method.
Related posts:
- Java Custom Annotations -
@Ruleand@Testare annotations the runner reads with reflection - How the JVM Works - Where class metadata lives, which JUnit uses to find rules and tests
- Testing an Android Database - The
@Beforedatabase wipe that a Rule would share across classes - Android Instrumentation Testing Using Espresso -
ActivityTestRuleis a Rule that launches an Activity - Instrumentation Testing of ListView - Another
ActivityTestRuleexample from the same era - Template Method Design Pattern - The lifecycle skeleton
@Before/@Test/@Afterfollows
Further reading: the JUnit 4 Rules wiki, the Rule and TestRule javadocs, Baeldung’s JUnit 4 Rules and JUnit 5 Extensions guides, and the current JUnit User Guide.