skip to content

What do JUnit 4's `Suite` and `Categories` runners do, and how do you tag a test class or method so a category filter selects it?

level: middleimportance: should knowfreq 26%

answer

  1. @RunWith(Suite.class) + @Suite.SuiteClasses({...}), empty body
  2. @ClassRule on a suite wraps every contained class
  3. Categories extends Suite — still needs SuiteClasses
  4. @Category(Marker.class) on class or method; subtypes match
  5. @Category on a suite has no effect

basics

~10 s

Suite aggregates other test classes: annotate an empty class @RunWith(Suite.class) and @Suite.SuiteClasses({A.class, B.class}). Categories extends Suite and filters by tag: mark classes or methods @Category(SlowTests.class) and add @IncludeCategory/@ExcludeCategory to the suite class.

solid answer

~40 s

**`Suite`** turns an otherwise empty class into an aggregator: ```java @RunWith(Suite.class) @Suite.SuiteClasses({OrderTest.class, PaymentTest.class}) public class ApiSuite {} ``` The class body stays empty except for `@ClassRule`/`@BeforeClass` members, which then wrap **every** class in the suite — the standard way to start a shared resource once. Suites nest, so a suite may list other suites. **`Categories`** extends `Suite` and adds filtering. Categories are usually empty marker interfaces (`public interface SlowTests {}`). Tag a test class or an individual method with `@Category(SlowTests.class)` — several categories per element are allowed — then declare a suite: ```java @RunWith(Categories.class) @Categories.IncludeCategory(SlowTests.class) @Categories.ExcludeCategory(FlakyTests.class) @Suite.SuiteClasses({OrderTest.class, PaymentTest.class}) public class SlowSuite {} ``` Subtypes of an included category also match. Note the documented limitation: annotating a **suite** with `@Category` has no effect — the tag must sit on the test class or the method.

code

java · 10 lines
java
@RunWith(Suite.class)
@Suite.SuiteClasses({OrderTest.class, PaymentTest.class})
public class ApiSuite {

    @ClassRule
    public static ExternalResource server = new ExternalResource() {
        @Override protected void before() { Server.start(); }
        @Override protected void after()  { Server.stop(); }
    };
}

go deeper

for a junior

Know the two annotation pairs — @RunWith(Suite.class) with @SuiteClasses, and @Category with @IncludeCategory — and that the suite class body is empty.

for a middle

Explain that Categories extends Suite, that marker interfaces are the convention, that subtypes match, and that a suite-level @ClassRule wraps every contained class.

for a senior

Address the manifest-rot problem, exclusion-beats-inclusion, the "no tests remain" failure, and when tagging beats splitting a class.

for a principal

Decide the suite/tag taxonomy for a large codebase — which axes deserve tags at all, and how to keep selection honest as the suite grows.

## Suite — aggregation as a runner `Suite` is simply a `Runner` whose children are other runners rather than methods. You declare it on an empty class: ```java @RunWith(Suite.class) @Suite.SuiteClasses({OrderTest.class, PaymentTest.class, ShippingTest.class}) public class ApiSuite {} ``` The annotated class is a *manifest*, not a test class. Omitting `@SuiteClasses` is an initialization error — "class must have a SuiteClasses annotation". Because `Suite` needs to build runners for the listed classes, its constructor takes a `RunnerBuilder` alongside the class, which is the second constructor form `@RunWith` supports. The body is normally empty, with one important exception: `@BeforeClass`, `@AfterClass` and `@ClassRule` members on the suite class apply around the *whole* suite. A `@ClassRule` on a suite starts a server before the first contained class and stops it after the last, which is the cheapest correct way to share an expensive resource across many classes. Suites compose: a suite may list other suites, giving a tree. `Enclosed` is a related runner that runs the nested classes of the annotated class instead of an explicit list, which keeps the manifest from drifting out of date. The eternal weakness of an explicit suite is exactly that manifest: a new test class that nobody adds to it silently never runs when the suite is the entry point. ## Categories — filtering by tag `Categories` extends `Suite`, so it still needs `@Suite.SuiteClasses` to say *which* classes are in scope; the runner then filters what runs inside them. **Defining a category.** Any type works, but the convention is an empty marker interface, because interfaces can extend one another and JUnit honours subtyping: ```java public interface SlowTests {} public interface DatabaseTests extends SlowTests {} ``` Including `SlowTests` also selects anything tagged `DatabaseTests`, because the runner matches subtypes of the included category. That gives you a hierarchy of tags for free. **Tagging.** `@Category` goes on a test class or on an individual test method and accepts several values: ```java @Category({SlowTests.class, SmokeTests.class}) @Test public void reconcilesLedger() { … } ``` A class-level `@Category` applies to the methods inside it, and a method-level tag adds to that. JUnit's documentation calls out one limitation explicitly: annotating a *suite* with `@Category` has no effect — categories must be on the direct class or method. **Filtering.** `@Categories.IncludeCategory(...)` keeps only matching tests; `@Categories.ExcludeCategory(...)` removes matching ones; both accept several categories. Exclusion wins over inclusion when a test matches both. ## What each is for - **Suite**: define an ordered, named collection — a smoke set, an integration set — or to hang a shared `@ClassRule` over many classes. - **Categories**: a cross-cutting tag that ignores package and class structure. "Everything touching the database" rarely lines up with a package, and that is precisely the shape categories handle. ## Practical cautions - Categories only filter *within* the classes the suite lists, so a tagged test in a class that is not listed does not run. Combining categories with an explicit `@SuiteClasses` list means keeping the list current. - An include filter that matches nothing yields "no tests remain", reported as a failure of the suite — confusing until you have seen it once. - Because the marker types are ordinary classes, a typo compiles fine but silently selects nothing. - Tags on classes tend to rot; if a class is 90% fast and 10% slow, that is a signal to split the class, not to tag it. ## Interview framing Say what each runner *is* (`Suite` aggregates classes; `Categories` is a `Suite` plus a filter), how tagging works (marker interfaces, `@Category` on class or method, subtypes match), name the suite-level `@ClassRule` trick, and mention the documented limitation that `@Category` on a suite is ignored.

  • If a test is tagged with a category but its class is not listed in the suite's `@SuiteClasses`, does it run?
    No. `Categories` extends `Suite`, so the set of candidate classes is exactly what `@SuiteClasses` names; the category annotations only filter within that set. This is the classic trap: a new tagged class is written, nobody adds it to the manifest, and it silently never executes through that suite.
  • Why are categories usually declared as empty marker interfaces rather than classes?
    Because JUnit matches subtypes of the included category, and interfaces give you multiple inheritance of tags — a test can carry several categories, and one category can extend another to form a hierarchy (`DatabaseTests extends SlowTests`), so including the parent also selects the children. A class-based marker works but constrains you to single inheritance.

saying these in an interview costs you the question

  • Putting test methods in the class annotated `@RunWith(Suite.class)` and expecting them to run — the suite class is a manifest, not a test class.
  • Using `@RunWith(Categories.class)` without `@Suite.SuiteClasses` and expecting it to scan the whole project.
  • Annotating a suite with `@Category` and expecting it to tag the contained classes — JUnit documents that as having no effect.
  • Assuming an included category does not match its subtypes; subtype matching is exactly what makes marker hierarchies useful.
  • Forgetting that an include filter matching nothing fails the suite with "no tests remain".

context