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?
answer
- default retention = CLASS = invisible to reflection
- RUNTIME or the test silently disappears
- METHOD for @Test bundles, TYPE for tags/extensions
- ANNOTATION_TYPE to let others compose yours
- @Inherited unnecessary — Jupiter searches hierarchy itself
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).
solid answer
~40 sTwo things. First, **`@Retention(RetentionPolicy.RUNTIME)`**. Java's default is `CLASS`: the annotation is compiled into the bytecode but not exposed to reflection, so Jupiter's lookup finds nothing and the method is quietly not a test — no error, it just disappears from the report. This is the usual cause of "my custom annotation stopped working". Second, **`@Target`**. Omitting it makes the annotation applicable almost everywhere, which compiles but lets people put a method-level test marker on a class where it does nothing. Use `ElementType.METHOD` for a `@Test` bundle, `ElementType.TYPE` for a class-level bundle of `@Tag` and `@ExtendWith`, and add `ANNOTATION_TYPE` if other annotations should compose yours. `@Inherited` is usually pointless here: it only covers class-level inheritance from a superclass in plain reflection, and Jupiter already searches superclasses and interfaces itself. `@Documented` is cosmetic.
code
java · 12 lines// BROKEN: default CLASS retention -> invisible to JUnit
@Target(ElementType.METHOD)
@Tag("slow")
@Test
public @interface SlowTest {}
// CORRECT
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Tag("slow")
@Test
public @interface SlowTest {}go deeper
Recall the rule: RUNTIME retention, and a @Target matching where you use the annotation.
Explain why CLASS retention fails silently and choose targets deliberately, including ANNOTATION_TYPE for composability.
Describe the diagnosis path (reflective check) and why @Inherited is not the fix, given Jupiter's own hierarchy search.
Treat custom annotations as shared API: reviewed declarations, a single package, and a smoke test asserting the annotation is discoverable.
## Retention: the silent killer Java has three retention policies. `SOURCE` discards the annotation at compile time. `CLASS` — the **default** — writes it into the class file but does not expose it to the reflection API. `RUNTIME` keeps it and makes it visible to `getAnnotations()`. JUnit Jupiter discovers tests reflectively, so an annotation without `@Retention(RetentionPolicy.RUNTIME)` does not exist as far as the engine is concerned. The failure mode is nasty because it is *silent*: no compile error, no discovery error, the annotated methods simply vanish from the executed set. If the class contains no other test methods, you also get an empty container. Diagnosis is straightforward once you suspect it: open the annotation type and look at its own annotations; or assert in a scratch test that `Foo.class.getMethod("bar").getAnnotation(MyTest.class)` is non-null. ## Target: what may be annotated `@Target` restricts the declaration contexts where your annotation is legal. - **`ElementType.METHOD`** — for a marker composed of `@Test` (or `@RepeatedTest`, `@ParameterizedTest`). Restricting it here means a misplaced usage on a class is a compile error rather than a no-op. - **`ElementType.TYPE`** — for a class-level bundle, typically `@Tag` plus `@ExtendWith` plus `@TestInstance`. Note that composing `@Test` into a `TYPE`-targeted annotation is legal Java but useless: a class is not a test method. - **`ElementType.ANNOTATION_TYPE`** — needed if you want *other* annotations to compose yours, since placing your annotation on an annotation type is itself a usage. If you omit `@Target` entirely, the annotation is applicable in all declaration contexts. It still works where you meant it to, but you have thrown away the compiler's help. The JUnit annotations you compose have their own targets, and they must permit `ANNOTATION_TYPE` for composition to compile at all. Jupiter's do: `@Test` is targeted at `{ANNOTATION_TYPE, METHOD}`, `@Tag` and `@ExtendWith` at annotation types as well. ## What you do not need - **`@Inherited`** applies only to type-level annotations and only to plain reflective lookup on subclasses. Jupiter implements its own search across superclasses and implemented interfaces, so class-level tags and extensions on a base test class already reach subclasses without it. Adding it is harmless but signals a misunderstanding. - **Registration.** There is no file, service loader entry or configuration parameter for composed annotations. They are pure annotation lookup. - **A JUnit dependency of its own.** The annotation lives in your test sources and needs the Jupiter API on the compile path, nothing more. ## A checklist for review 1. `@Retention(RUNTIME)` present? 2. `@Target` narrowed to the intended context, including `ANNOTATION_TYPE` if it should be composable? 3. Do the composed JUnit annotations actually mean what you want at that level (a `@Test` on a `TYPE` target is a bug)? 4. Is the annotation in a package the whole test source set can see?
- How would you prove quickly that retention is the problem rather than something in discovery?Write a one-line reflective check in a scratch test: `assertNotNull(MyTests.class.getDeclaredMethod("someTest").getAnnotation(SlowTest.class))`. With CLASS retention it returns null even though the source clearly has the annotation. That isolates the cause to the annotation declaration and rules out engine or selection issues.
- Should a class-level composed annotation also carry @Inherited so subclasses pick it up?It is not required. Jupiter searches superclasses and implemented interfaces for class-level annotations such as @Tag and @ExtendWith as part of its own lookup, so an abstract base test class already shares them with subclasses. `@Inherited` only changes plain JDK reflection behaviour and does nothing extra here.
saying these in an interview costs you the question
- Assuming annotations are visible to reflection by default
- Adding @Inherited and claiming that is what makes discovery work
- Leaving @Target off and treating it as equivalent to a precise target
- Expecting a compile error or a discovery error when retention is wrong