skip to content

Test Annotations

The annotations that mark tests and shape how they run — @Test, the lifecycle hooks, @Disabled, @DisplayName, @Nested, @Tag. Asked as a quick check that you speak Jupiter's vocabulary rather than JUnit 4's.

on this pageshow

explore

questions

22

In JUnit 5, some teams mark tests with one custom annotation such as @SlowIntegrationTest instead of writing @Test plus @Tag("slow") on every method. What is that custom annotation called, and how does JUnit know to treat the method as a test?

level: juniorimportance: must knowfreq 45%

answer

  1. annotation on an annotation = meta-annotation
  2. directly present OR meta-present
  3. @Retention(RUNTIME) or invisible
  4. @Target METHOD / TYPE / ANNOTATION_TYPE
  5. one name = @Test + tags + extensions

basics

~20 s

It is a composed annotation: your own annotation type that is itself annotated with JUnit's @Test, @Tag, @ExtendWith and so on. Jupiter looks up annotations recursively, so anything meta-annotated with @Test is discovered and run as a test.

solid answer

~40 s

That is a **composed annotation**; the JUnit annotations placed on it are its *meta-annotations*. You declare your own annotation type and annotate the annotation itself with `@Test`, `@Tag("slow")`, `@Tag("integration")`, plus `@Retention(RUNTIME)` and a `@Target`. JUnit Jupiter never asks only whether `@Test` is *directly present* on a method; it walks the annotations of the annotations recursively. A method annotated `@SlowIntegrationTest` is therefore meta-annotated with `@Test`, is discovered exactly as if `@Test` were written there, and carries both tags. The payoff is one vocabulary word per test category: the marker, the tags and any extensions live in one place, so changing the convention is one edit instead of hundreds. The two mandatory pieces are `@Retention(RetentionPolicy.RUNTIME)` — without it the annotation is invisible to reflection — and a `@Target` that includes the element you intend to annotate.

code

java · 15 lines
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("slow")
@Tag("integration")
@Test
public @interface SlowIntegrationTest {
}

class OrderRepositoryTest {

    @SlowIntegrationTest
    void persistsOrderAcrossRestart() {
        // discovered as a test; tagged slow + integration
    }
}

go deeper

for a junior

Know the term 'composed annotation', that JUnit finds @Test through it, and that @Retention(RUNTIME) is mandatory.

for a middle

Explain directly-present versus meta-present lookup and pick sensible @Target values for method-level versus class-level bundles.

for a senior

Discuss what belongs in the vocabulary (tags plus extensions), the indirection cost, and the absence of attribute aliasing.

for a principal

Frame it as codebase-wide taxonomy: a small set of named test categories that selection and reporting are built on, owned and reviewed like any public API.

## Meta-annotations and composition A Java annotation is a marker attached to a class, method or field. An annotation *type* can itself carry annotations, and those are called **meta-annotations**. A **composed annotation** is a custom annotation type that bundles several JUnit Jupiter annotations behind one name. JUnit 5 (the Jupiter programming model) was designed for this from the start: `@Test`, `@Tag`, `@ExtendWith`, `@TestInstance`, `@DisplayNameGeneration` and friends are all declared with `@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})` or `...TYPE)`, so they may legally be placed on another annotation. ## How discovery sees it When the JUnit Platform discovers tests, Jupiter does not use plain `element.getAnnotation(Test.class)`. It uses the platform's annotation support, which considers an annotation *present* if it is: 1. **directly present** on the element, or 2. **meta-present** — reachable by recursively following the annotations of the annotations, to arbitrary depth. So `@SlowIntegrationTest` → `@Test` makes the method a test method; `@SlowIntegrationTest` → `@Tag("slow")` makes the tag apply. You can compose composed annotations (`@NightlyIntegrationTest` meta-annotated with `@SlowIntegrationTest`), and the lookup still finds `@Test`. ## Writing one correctly Three declaration details matter: - `@Retention(RetentionPolicy.RUNTIME)` is **required**. The Java default is `CLASS`, which keeps the annotation in the bytecode but hides it from reflection, so JUnit sees nothing and the method silently is not a test. - `@Target` should list what you mean to annotate: `METHOD` for a test marker, `TYPE` for a class-level bundle (tags plus `@ExtendWith`), and `ANNOTATION_TYPE` if you want others to compose on top of yours. - Nothing else is required. `@Documented` is nice for Javadoc; `@Inherited` is largely irrelevant because Jupiter runs its own hierarchy search rather than relying on JDK annotation inheritance. ## Why teams do it - **One edit, whole category.** Adding an extension to every integration test becomes a single line on the annotation. - **Intent over mechanics.** `@AcceptanceTest` reads better than four stacked annotations, and it is greppable. - **Consistency.** Tag strings are typo-prone; a composed annotation turns a string into a compile-checked name. The cost is indirection: a reader who has never seen `@AcceptanceTest` must open it to learn that it means `@Test` plus two tags and a Testcontainers extension. Keep the vocabulary small and the annotations discoverable in one package. One limit to remember: Jupiter has no attribute aliasing. Your annotation cannot take a parameter and forward it into the meta-annotation it composes — the composed values are fixed at declaration time.

  • Can a composed annotation be composed of another composed annotation?
    Yes. Jupiter's lookup is recursive and depth-unbounded, with cycle protection, so `@NightlyIntegrationTest` meta-annotated with `@SlowIntegrationTest` still resolves to `@Test` and to every tag on the way. Keep the chain shallow, though: each extra hop is another file a reader must open to know what a test actually does.
  • Did JUnit 4 support this?
    Not really. JUnit 4 checks for `@Test` directly on the method, so you could not hide it behind a custom annotation; the closest equivalents were runners and rules, and category marker interfaces. Meta-annotation lookup is a Jupiter feature and is one of the concrete reasons teams cite for migrating.

saying these in an interview costs you the question

  • Thinking the custom annotation needs a JUnit-provided base class or registration somewhere
  • Omitting @Retention(RUNTIME) and then blaming the build for not finding the test
  • Believing Jupiter only reads annotations that are directly present
  • Claiming the composed annotation replaces the engine or requires a custom runner

context

open as a page

In JUnit 5, what does the @Disabled annotation do, how does putting it on a test class differ from putting it on a single test method, and what is the string argument used for?

level: juniorimportance: must knowfreq 50%

basics

~20 s

@Disabled skips a test instead of running it, reporting it as skipped rather than failed. On a method it skips that method; on a class it skips every test in the class, including its nested classes. The string argument is the reason, shown in reports and IDEs.

open as a page

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?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Annotate 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.

open as a page

In JUnit 5, how do you make a single test fail automatically if it runs longer than a chosen duration, and what exactly happens when that limit is reached?

level: juniorimportance: must knowfreq 40%

basics

~20 s

Annotate the test with JUnit Jupiter's @Timeout, e.g. @Timeout(value = 500, unit = TimeUnit.MILLISECONDS). The default unit is seconds. When the limit elapses the test fails with a TimeoutException naming the method and the configured duration.

open as a page

You wrote a custom JUnit 5 annotation that bundles @Test and @Tag, but methods marked with it are no longer picked up as tests. Which Java meta-annotations must your annotation type itself declare, and what goes wrong when each is missing or wrong?

level: middleimportance: must knowfreq 38%

basics

~20 s

@Retention(RetentionPolicy.RUNTIME) is mandatory — the default CLASS retention hides the annotation from reflection, so JUnit never sees it. @Target must include the element you annotate (METHOD for a test marker, TYPE for class-level, ANNOTATION_TYPE to allow further composition).

open as a page

In JUnit 5 (Jupiter), how do you make a single test method execute several times in a row, and how do you keep each execution distinguishable in the test report?

level: juniorimportance: should knowfreq 40%

basics

~20 s

Replace @Test with @RepeatedTest(n). Jupiter runs the method n times as n separate tests, each with its own @BeforeEach/@AfterEach. Default names read "repetition 1 of 10"; customise them with the annotation's name attribute using {currentRepetition} and {totalRepetitions}.

open as a page

How do you control the human-readable name a JUnit 5 test shows in IDE output and reports, and when is that better than encoding the description in the method name?

level: juniorimportance: should knowfreq 52%

basics

~20 s

Put @DisplayName("...") on the test class or method. It accepts arbitrary text — spaces, punctuation, non-ASCII — and replaces the method name in IDE and report output. It only affects display; the method name is still the identity used for selection and unique IDs.

open as a page

If a JUnit 5 test method is annotated @Disabled, which lifecycle callbacks still run — @BeforeAll, @BeforeEach, @AfterEach, @AfterAll — and is the test class instantiated? How does the answer change when the whole class is disabled?

level: middleimportance: should knowfreq 30%

basics

~20 s

For a disabled method the class container still runs, so @BeforeAll and @AfterAll execute, but no instance is created for that method and its @BeforeEach/@AfterEach are skipped. For a disabled class nothing runs at all — no @BeforeAll, no @AfterAll, no instantiation.

open as a page

A JUnit 5 test only makes sense on Linux and only when the system property db.url is set. Compare marking it unconditionally @Disabled with using JUnit 5's conditional annotations such as @EnabledOnOs and @EnabledIfSystemProperty — which do you choose, and why does it matter?

level: middleimportance: should knowfreq 28%

basics

~20 s

Use the conditional annotations. @Disabled is unconditional: the test never runs anywhere, so you lose the signal even on machines where it would work. @EnabledOnOs(LINUX) and @EnabledIfSystemProperty run it exactly where its prerequisites hold and report it as skipped elsewhere, with the condition as the reason.

open as a page

A JUnit 5 test method annotated with @RepeatedTest needs to know which iteration it is currently on — for example to seed data differently on the first run. How do you get that information, and where else can it be injected?

level: middleimportance: should knowfreq 30%

basics

~20 s

Declare a RepetitionInfo parameter on the method; JUnit's built-in resolver injects it. It exposes getCurrentRepetition(), getTotalRepetitions() and getFailureThreshold(). It can also be injected into @BeforeEach/@AfterEach — but only when that callback is running for a repeated test.

open as a page

JUnit 5 can derive readable test labels from method names automatically instead of requiring an annotation on every method. How does that mechanism work, and which strategies ship with the framework?

level: middleimportance: should knowfreq 33%

basics

~20 s

Annotate the class with @DisplayNameGeneration(SomeGenerator.class). Built-ins are Standard (method name plus parentheses), Simple (drops empty parentheses), ReplaceUnderscores (underscores become spaces) and IndicativeSentences (prefixes enclosing class names). You can implement DisplayNameGenerator yourself, or set a suite-wide default via a configuration parameter.

open as a page

You place JUnit Jupiter's @Timeout on a test class rather than on one method. Which methods does it then govern, and how is the effective limit resolved if a method or an inner class also declares one?

level: middleimportance: should knowfreq 24%

basics

~20 s

A class-level @Timeout applies to every testable and lifecycle method in that class and in its @Nested classes. The most specific declaration wins: a method-level annotation overrides its class, an inner class overrides the enclosing class, and any annotation overrides suite-wide defaults.

open as a page

How can you apply a default time limit to every test in a JUnit 5 suite without annotating them individually, and how would you switch those limits off while stepping through a test in a debugger?

level: middleimportance: should knowfreq 22%

basics

~10 s

Set the JUnit Platform configuration parameter junit.jupiter.execution.timeout.default, e.g. '5 s', in a junit-platform.properties file on the test classpath. Setting junit.jupiter.execution.timeout.mode to disabled_on_debug makes Jupiter ignore all timeouts when a debugger is attached.

open as a page

Explain how JUnit Jupiter's annotation lookup actually resolves an annotation such as @Tag or @ExtendWith: how deep does it follow annotations of annotations, and where besides the element itself does it search?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Jupiter treats an annotation as present if it is directly on the element or meta-present anywhere up the annotation graph, searched recursively with cycle protection. For classes it also searches superclasses and implemented interfaces, and enclosing classes for @Nested. Repeatable annotations such as @Tag and @ExtendWith are collected from all of those sources, not overridden.

open as a page

How does JUnit 5 decide at runtime that a test is disabled, and how would you implement your own rule — for example skipping tests that need an external service when that service is unreachable? Is there a way to force such skipped tests to run anyway?

level: seniorimportance: should knowfreq 22%

basics

~20 s

Jupiter asks every registered ExecutionCondition extension before running a node; @Disabled is itself the built-in DisabledCondition. You implement ExecutionCondition, return ConditionEvaluationResult.disabled(reason) or enabled(reason), and register it with @ExtendWith. The config parameter junit.jupiter.conditions.deactivate switches conditions off by pattern so disabled tests run.

open as a page

A teammate proposes annotating a suspected intermittently-failing JUnit 5 test with @RepeatedTest(50) and leaving it in CI to catch the problem. How do you evaluate that proposal, and what would you do instead?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Repetition only catches flakes whose cause varies run to run in the same JVM and thread — random data, timing, leaked static or database state. It misses ordering, cross-test and concurrency flakes, multiplies CI time by 50, and adds no retry. Use it temporarily to reproduce and later to prove a fix; fix the root cause.

open as a page

JUnit Jupiter's @Timeout annotation accepts a threadMode of SAME_THREAD or SEPARATE_THREAD. Explain how each one executes the annotated method and the risks of choosing either.

level: seniorimportance: should knowfreq 26%

basics

~20 s

SAME_THREAD runs the method on the calling thread and merely interrupts it at the deadline, so uninterruptible code keeps running. SEPARATE_THREAD runs it on another thread and fails immediately at the deadline, abandoning that thread and losing thread-bound state such as transactions.

open as a page

A JUnit 5 method annotated @RepeatedTest(100) keeps executing all remaining repetitions even after the first several have failed, wasting minutes of build time. How can you make it stop early, and what are the constraints on that setting?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

Set the annotation's failureThreshold attribute, e.g. @RepeatedTest(value = 100, failureThreshold = 3). Once that many repetitions have failed, the remaining ones are automatically skipped rather than executed. It must be positive and smaller than the repetition count; it was added in JUnit 5.10.

open as a page

A Java codebase has several thousand JUnit 5 tests whose names appear as terse, unreadable identifiers in CI reports. How would you get consistent, human-readable names across the suite without editing every test method, and what would you watch out for?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Set junit.jupiter.displayname.generator.default in a junit-platform.properties file on the test classpath to a generator (ReplaceUnderscores, or a custom camelCase splitter). Existing @DisplayName annotations still win, so nothing is clobbered. Watch out: labels are not identities, so tooling keyed on displayed names will churn.

open as a page

How would you decide whether a large JUnit 5 test codebase should adopt custom composed annotations that bundle @Test with tags and extensions, versus having every test repeat the standard annotations?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Introduce a composed annotation when a category is real, stable and repeated many times, and when its tags and extensions must stay consistent. Keep the vocabulary small, one shallow level, documented in one package. Prefer explicit standard annotations for one-off or evolving setups.

open as a page

A test suite contains forty methods annotated @Disabled, some of them for more than a year. How would you handle disabled tests as a matter of engineering policy?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Treat disabled tests as tracked debt: every one needs a reason plus a ticket, they are inventoried and reported, and each is triaged into fix, delete, or convert to a condition. Environment-dependent ones should never be @Disabled. Make skip counts visible so green builds stop hiding lost coverage.

open as a page

Your JUnit 5 suite occasionally wedges in CI and a job has to be cancelled manually. How would you design a time-limit policy across the suite, and what are the tradeoffs of the options you would consider?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Set a generous suite-wide default via junit.jupiter.execution.timeout.default as a deadlock net, tighten only where a hang is plausible, keep the default same-thread mode, disable timeouts on debug, and add an out-of-JVM job watchdog because interrupt-based limits cannot stop uninterruptible code.

open as a page