SkillByAIOpen interactive version →

Lesson 5 / 25

Lifecycle, Nested Tests, Tags and Display Names

Use lifecycle methods, test instance lifecycle, @Nested, @Tag and @Disabled.

Organising and setting up tests

Lifecycle methods run around tests: @BeforeEach and @AfterEach before and after every test method, @BeforeAll and @AfterAll once per class (they must be static unless the class uses @TestInstance(Lifecycle.PER_CLASS)). By default JUnit creates a new instance of the test class for every test method (PER_METHOD), so instance fields are fresh each time and tests cannot leak state through them. @Nested inner classes group related tests with their own setup, producing readable hierarchies such as "Wallet / when empty / pay throws". @DisplayName gives readable names, and a @DisplayNameGeneration strategy can derive them from method names. @Tag("slow") labels tests so builds can include or exclude groups (for example running only fast tests on each commit). @Disabled("reason") skips a test, but always give a reason and a ticket, otherwise disabled tests rot forever. @RepeatedTest(n) runs a test several times, useful for spotting flakiness.

Nested structure with shared setup

Each nested class adds its own context to the outer setup.

@DisplayName("Shopping cart")
class CartTest {

    private Cart cart;

    @BeforeEach
    void createCart() {
        cart = new Cart();                     // fresh for every test (PER_METHOD lifecycle)
    }

    @Nested
    @DisplayName("when empty")
    class WhenEmpty {
        @Test
        @DisplayName("total is zero")
        void totalIsZero() { assertThat(cart.totalPaise()).isZero(); }

        @Test
        @DisplayName("checkout is rejected")
        void checkoutRejected() { assertThatThrownBy(cart::checkout).isInstanceOf(IllegalStateException.class); }
    }

    @Nested
    @DisplayName("with two items")
    class WithTwoItems {
        @BeforeEach
        void addItems() {
            cart.add("pen", 2, 4_950);
            cart.add("ink", 1, 12_000);
        }

        @Test
        void totalSumsLines() { assertThat(cart.totalPaise()).isEqualTo(21_900); }

        @Test
        @Tag("slow")
        void checkoutCreatesOrder() { assertThat(cart.checkout().lines()).hasSize(2); }
    }
}

Chapters and sections in a manual

Nested test classes are like a manual's chapters and sections: "Cart → when empty → total is zero". Readers find the rule they care about without scanning one long, flat list.

Quick check: By default, how many test-class instances does JUnit Jupiter create for a class with five test methods?

  • One shared instance
  • Five, one per test method
  • Two
  • None; tests are static
Answer

Five, one per test method — The default PER_METHOD lifecycle creates a fresh instance for each test method.