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?
answer
- taxonomy, not a performance feature
- few names, one shallow level, one owning package
- accumulation is one-way — no subtracting tags/extensions
- no parameters: one annotation per value
- don't alias a single @Test
basics
~20 sIntroduce 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.
solid answer
~60 sI treat the annotation set as a **taxonomy of test categories**, and a taxonomy is only worth having when the categories are real: they map to something the team actually decides on — where a test runs, how long it may take, what infrastructure it needs. Criteria for creating one: the pattern repeats across many classes; the bundle (tags plus extensions plus `@TestInstance` choices) must not drift; and the category is stable enough that renaming it is not routine. Criteria against: the setup is one-off; the group is still forming; or the annotation would just alias a single standard annotation, which trades a familiar symbol for an unfamiliar one. Guardrails I insist on: a handful of names, not dozens; one shallow composition level so effective configuration stays traceable; a single package that owns them, with Javadoc stating exactly what each expands to; and a review reflex that adding an extension to a widely used annotation is a change to hundreds of tests. The cost is always the same: indirection. Newcomers must open the annotation to know what a test does.
go deeper
Say that a custom annotation is worth it when the same annotations are repeated a lot, and that it must be documented.
Weigh the single-point-of-change benefit against indirection, and note the lack of parameters.
Add the one-way accumulation constraint and blast-radius reasoning for editing a widely used category annotation.
Frame it as governed taxonomy with explicit guardrails — size, depth, ownership, review policy — and name the conditions under which you would collapse it back to plain annotations.
## What the decision actually is Composed annotations are not a performance or correctness feature — every one of them can be replaced by writing the same annotations out. The decision is about **naming a category** and about **where consistency is enforced**. So the honest framing is: is there a category worth a name, and does it change often enough that having one place to change it pays for the indirection? ## Arguments for a composed vocabulary - **Single point of change.** If integration tests must all use a container extension, adding it to `@IntegrationTest` is one line rather than a mass edit that some new file will inevitably miss. - **Consistency by construction.** Raw `@Tag("integr8tion")` strings are typos waiting to happen; an annotation is a compile-checked identifier with IDE completion and safe rename. - **Intent at the call site.** `@AcceptanceTest void checkoutSucceeds()` states a classification that four stacked annotations only imply. - **Enforceability.** A small closed set of category annotations is something you can check for in review or with a static rule — every test must carry exactly one. ## Arguments against - **Indirection.** Standard annotations are universal knowledge; your annotation is local knowledge. The reader must open it, and if it composes another composed annotation, open that too. - **Accumulation is one-way.** Jupiter aggregates repeatable annotations such as `@Tag` and `@ExtendWith` from every level of the search and offers no way to subtract. A category that grows extensions will impose them on tests that outgrew the category, and the only escape is a new annotation. - **No parameters.** Because Jupiter has no attribute aliasing, a parameterised category multiplies into one annotation per value. Twenty feature tags become twenty annotation types, at which point plain `@Tag` strings may honestly be better. - **Premature taxonomy.** Categories invented before the suite has a shape tend to be wrong, and a wrong category annotated onto 400 tests is expensive to unwind. ## The policy I would set 1. **Few names, chosen from how the team already talks** — typically speed/scope tiers plus one or two infrastructure needs. 2. **One level of composition**, two only with a stated reason. Depth is the main driver of "what does this test actually do?" confusion. 3. **One package owns them**, with Javadoc that literally lists the expansion. This is the substitute for reading the source. 4. **Category annotations are reviewed like public API.** Adding an extension to `@IntegrationTest` changes every test using it; that is a change with a blast radius, and the PR description should say so. 5. **Do not alias a single annotation.** `@MyTest` that is only `@Test` is pure cost. 6. **Prefer composition over a fat abstract base class** for cross-cutting setup. Both share configuration, but the annotation is visible at the usage site and does not consume the single inheritance slot; the base class hides the same dependencies behind `extends`. ## How I would sanity-check it later If a newcomer cannot state what a test does from the annotation name plus its Javadoc, the vocabulary has failed. If the set has grown past roughly a dozen names, or several annotations are near-duplicates that differ by one extension, the taxonomy has drifted and should be collapsed back toward explicit annotations.
- When would you prefer an abstract base test class over a composed annotation for shared setup?When the shared thing is real code — fields, helper methods, a configured fixture object — rather than pure configuration. Extensions and tags belong on an annotation because it keeps the dependency visible at the usage site and leaves the single inheritance slot free; behaviour a test actually calls is more natural on a base class, and even then an extension plus parameter resolution is often cleaner.
- A team wants a per-feature tag on every test, roughly thirty values. Composed annotations or plain @Tag strings?Plain strings, or a generated constant per feature. Jupiter cannot forward an attribute into @Tag, so thirty values would mean thirty annotation types that add no meaning beyond the string they wrap. Composed annotations pay off for a small closed set of categories, not for an open-ended, frequently changing dimension.
saying these in an interview costs you the question
- Introducing an annotation per test class, producing a vocabulary nobody can hold in their head
- Claiming composed annotations improve execution performance
- Assuming a category annotation can later opt a test out of an inherited tag or extension
- Designing a parameterised category annotation, unaware that Jupiter has no attribute aliasing