What rules must a Java method (and its enclosing class) satisfy for the JUnit 5 Jupiter engine to discover and run it as a test?
answer
- org.junit.jupiter.api.Test — not org.junit.Test
- Not private, not static, not abstract, returns void
- Params only if a ParameterResolver supplies them
- Class: non-abstract, single constructor, need not be public
- @Nested + non-static for inner test classes
basics
~20 sAnnotate it with org.junit.jupiter.api.Test. The method must not be private, static or abstract, and must return void. Parameters are allowed only if a ParameterResolver supplies them (TestInfo, TestReporter, @TempDir...). The class must not be abstract and needs a single constructor; neither class nor method has to be public.
solid answer
~50 sThe method needs `@Test` from **`org.junit.jupiter.api`** — importing JUnit 4's `org.junit.Test` by accident is the classic "my test never runs". Then: - **not `private`** — package-private, protected and public all work, and package-private is idiomatic in Jupiter; - **not `static`**, **not `abstract`**; - **returns `void`** — returning a value was deprecated in 5.11 and fails in current 5.x; - **parameters only if resolvable** — Jupiter injects `TestInfo`, `TestReporter`, `RepetitionInfo`, `@TempDir` paths, and anything a registered `ParameterResolver` (e.g. Spring's or Mockito's extension) supplies; an unresolvable parameter is a `ParameterResolutionException`, not a skip. For the class: it must not be `abstract`, must have exactly **one** constructor (which may itself take resolvable parameters), and — unlike JUnit 4 — need not be public. Inner classes must be non-static and annotated `@Nested` to be discovered. Also note Jupiter's `@Test` has **no attributes**: no `expected`, no `timeout`. Those became `assertThrows` and a separate annotation.
code
java · 18 linesimport org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertTrue;
class InvoiceWriterTest { // package-private class is fine
@Test
void writesFile(@TempDir Path dir) { // resolvable parameter
assertTrue(new InvoiceWriter().write(dir).toFile().exists());
}
@Test
void reportsItsOwnName(TestInfo testInfo) {
assertTrue(testInfo.getDisplayName().length() > 0);
}
}go deeper
Recall the checklist: right import, non-private, non-static, void return, non-abstract class — and that public is not required.
Add why parameters are permitted (ParameterResolver injection, with TestInfo/TestReporter/@TempDir as built-ins) and the single-constructor rule for the class.
Turn it into a diagnostic: distinguish silent non-discovery (wrong import, missing @Nested) from reported configuration errors (private/static/non-void), and mention the per-method instance lifecycle.
Explain the design rationale — attribute-free @Test pushing expected exceptions into assertThrows, and injection as the extension seam that lets Spring/Mockito integrate without inheritance.
## The annotation itself A JUnit 5 test method is one annotated with `org.junit.jupiter.api.Test`. The single most common beginner failure is importing `org.junit.Test` — the JUnit 4 annotation — which the Jupiter engine does not recognise at all. The symptom is not an error; it is a method that is silently never executed. If a test "does nothing", check the import first. Jupiter's `@Test` is also deliberately **attribute-free**. JUnit 4's `@Test(expected = ..., timeout = ...)` has no counterpart: expected exceptions are asserted with `assertThrows(...)` inside the body, and time limits live in a separate annotation. The rationale is that both concerns are assertions about behaviour, not metadata about discovery, and `expected` was too coarse — it passed if the exception came from anywhere in the method, including the setup lines. ## Method-level rules A discovered method must satisfy all of the following: - **Must not be `private`.** Jupiter accepts `public`, `protected` and package-private (default) visibility. Package-private is the idiomatic style in Jupiter, because tests are not an API for anyone outside the package and dropping `public` reduces noise. A `private` `@Test` method is a configuration error, not a silent skip — the engine reports it. - **Must not be `static`.** Test methods run against an instance. `static` is reserved for `@BeforeAll` / `@AfterAll` (unless the class uses `@TestInstance(PER_CLASS)`, which lifts that requirement for those callbacks). - **Must not be `abstract`.** An abstract method has no body to run. Abstract *base classes* declaring concrete `@Test` methods are fine and are a legitimate pattern — the tests are inherited by the concrete subclasses and run there. - **Must return `void`.** Historically a non-void return was ignored; JUnit 5.11 deprecated it with a warning, and current 5.x releases fail the test outright with a clear message. This bites Kotlin and fluent-assertion users who write expression bodies such as `fun test() = assertThat(x).isEqualTo(y)`, which return a value; the fix is an explicit block body returning `Unit`/`void`. - **Parameters are allowed, but only resolvable ones.** Unlike JUnit 4, Jupiter test methods may declare parameters. Every parameter must be supplied by a registered `ParameterResolver`. Built in, you get `TestInfo` (display name, tags, the test method/class), `TestReporter` (publish key/value entries into the report), `RepetitionInfo` (for repeated tests) and `@TempDir` for `Path`/`File`. Extensions add more — Spring's test extension injects beans, Mockito's injects `@Mock` parameters. An unresolvable parameter produces a `ParameterResolutionException` at execution time. ## Class-level rules - **The class must not be abstract**, or there is nothing to instantiate. - **It must have exactly one constructor.** Multiple constructors are ambiguous to the engine. The single constructor may itself declare resolvable parameters — a common way to receive an injected dependency once for the class. - **It need not be public.** JUnit 4 required public test classes; Jupiter does not, because it uses reflection with the necessary access. Package-private test classes are normal. - **Inner classes need `@Nested`** and must be **non-static** inner classes. A plain static nested class without `@Nested` is not scanned as a test class by default discovery. - Discovery itself also depends on the class being on the test classpath and matching the discovery filters, but within a normal test source set, the rules above are what govern whether your method runs. ## Lifecycle context By default (`Lifecycle.PER_METHOD`) the engine constructs a **new instance of the test class for every test method**, which is why instance fields are safe to mutate in a test: the next test gets a fresh object. `@TestInstance(Lifecycle.PER_CLASS)` switches to one instance for the whole class, which lets `@BeforeAll` be non-static but makes leftover field state a real hazard. ## Quick diagnostic checklist for "my test doesn't run" 1. Wrong `@Test` import (`org.junit` instead of `org.junit.jupiter.api`). 2. Method is `private`. 3. Method is `static`. 4. Class is abstract, or has more than one constructor. 5. Nested class missing `@Nested`, or declared `static`. 6. Method returns a value instead of `void`. 7. An unresolvable parameter on the method or constructor. Each of these is either an outright error the engine reports or, in the import case, a silently invisible method — and knowing which is which is the difference between a two-minute fix and an afternoon.
- Why is a JUnit 5 test method allowed to declare parameters at all, when JUnit 4 forbade it?Jupiter builds injection into its extension model: every parameter is supplied by a registered ParameterResolver. That gives the framework a uniform way to hand a test the things it needs — TestInfo, TestReporter, a @TempDir path, a Spring bean, a Mockito mock — without static holders or setup boilerplate. The cost is that an unresolvable parameter is a runtime ParameterResolutionException rather than a compile-time problem.
- A colleague's @Test method compiles but never appears in the report. What do you check first?The import. org.junit.Test is JUnit 4's annotation and the Jupiter engine ignores it entirely, so the method is simply never discovered — no error, no skip, nothing in the report. After that, check whether the method is private or static, whether the class is abstract or has more than one constructor, and whether an inner test class is missing @Nested or is declared static.
saying these in an interview costs you the question
- Insisting test classes and methods must be public (that was JUnit 4).
- Using @Test(expected = SomeException.class) — Jupiter's @Test has no attributes; use assertThrows.
- Making a test method static.
- Assuming a non-void return value is harmlessly ignored.
- Thinking a method with parameters cannot be a Jupiter test.