How do you write a basic unit test in JUnit 5, and what do the core annotations (@Test, @BeforeEach, @AfterEach, @BeforeAll, @AfterAll) do?
answer
- @Test marks a method as a test
- BeforeEach/AfterEach = per test; BeforeAll/AfterAll = once + static
- assertEquals(expected, actual) — expected first
- new test-class instance per method = isolation
- Arrange-Act-Assert
basics
~10 sA unit test is a method marked @Test that checks one behavior using assertions like assertEquals. @BeforeEach/@AfterEach run setup/cleanup before and after every test; @BeforeAll/@AfterAll run once for the whole class.
solid answer
~40 sIn JUnit 5 (JUnit Jupiter) a test is a method annotated @Test inside a test class; you don't need it to be public anymore. Inside, you arrange inputs, call the code under test, and assert the result with the static methods on org.junit.jupiter.api.Assertions (assertEquals, assertTrue, assertThrows, etc.). Lifecycle annotations control fixtures: @BeforeEach and @AfterEach run before/after every test method to give each test a fresh, isolated state; @BeforeAll and @AfterAll run once per class and must be static (unless the class is @TestInstance(PER_CLASS)) — use them for expensive shared setup like starting a container. JUnit creates a new test-class instance per test method by default, reinforcing isolation. You can also name tests with @DisplayName and disable them with @Disabled.
code
java · 23 linesimport org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private Calculator calc;
@BeforeAll static void bootDatabase() { /* once, must be static */ }
@BeforeEach void setUp() { calc = new Calculator(); } // fresh per test
@Test
@DisplayName("adds two positive numbers")
void addsTwoNumbers() {
assertEquals(5, calc.add(2, 3)); // expected, actual
}
@Test
void rejectsDivideByZero() {
assertThrows(ArithmeticException.class, () -> calc.divide(1, 0));
}
@AfterEach void tearDown() { /* per-test cleanup */ }
@AfterAll static void shutdown() { /* once, must be static */ }
}go deeper
Can write a @Test method, use assertEquals/assertTrue, and knows @BeforeEach sets up before each test. Understands one assertion-failure fails the test.
Knows the full lifecycle (Each vs All, static requirement), uses assertThrows and @DisplayName, and explains why per-method instances give isolation.
Connects JUnit to the broader discipline (pyramid, AAA, isolation), uses @TestInstance(PER_CLASS), @Nested, parameterized tests, and chooses fixture scope deliberately for speed vs isolation.
Sets team conventions for fixtures and isolation, weighs @BeforeAll-shared expensive resources (containers) vs per-test cost, and governs the JUnit Platform extension model across the codebase.
## What this is about **Unit testing** means writing small programs (tests) that automatically check that a piece of your production code (a *unit* — usually one class or method) behaves correctly, without a human clicking through the app. **JUnit** is the de-facto standard *test framework* for Java: it provides the vocabulary (annotations) for marking which methods are tests, and a *test runner* that discovers and executes them, then reports pass/fail. **JUnit 5** is the current major version. It is actually three pieces: **JUnit Platform** (the engine launcher that IDEs/Gradle/Maven talk to), **JUnit Jupiter** (the new programming model — the annotations and assertions you write against), and **JUnit Vintage** (a compatibility engine that runs old JUnit 4 tests). When people say "JUnit 5" in code they almost always mean **Jupiter**, in package `org.junit.jupiter.api`. ## A minimal test ```java import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class CalculatorTest { @Test void addsTwoNumbers() { Calculator calc = new Calculator(); // Arrange int result = calc.add(2, 3); // Act assertEquals(5, result); // Assert } } ``` - `@Test` marks `addsTwoNumbers` as a test. JUnit finds every `@Test` method and runs it. - `assertEquals(expected, actual)` is a **static assertion**: if the two values differ, it throws an `AssertionError` and JUnit records the test as *failed*; if they match, the test passes. The order is **expected first, actual second** — getting this backwards makes failure messages misleading. - Note there is no `public` and no `main`. In JUnit 5 test classes and methods can be package-private; the framework uses reflection to call them. ## The Arrange–Act–Assert (AAA) shape A clean unit test has three phases: **Arrange** (build the inputs and the object under test), **Act** (invoke the one behavior you're testing), **Assert** (verify the outcome). One test should check *one* logical behavior so that a failure points at exactly one cause. ## Lifecycle annotations (fixtures) A **fixture** is the fixed setup a test needs. JUnit gives you hooks so you don't copy setup into every test: | Annotation | When it runs | Typical use | |---|---|---| | `@BeforeEach` | before **every** `@Test` method | create fresh objects so tests don't share state | | `@AfterEach` | after **every** `@Test` method | clean up (close a file, clear a list) | | `@BeforeAll` | **once**, before all tests in the class | expensive one-time setup (start a DB/container) | | `@AfterAll` | **once**, after all tests in the class | tear that one-time thing down | ```java class OrderServiceTest { private OrderService service; @BeforeEach void setUp() { service = new OrderService(); } // fresh per test @Test void placesOrder() { /* uses a clean service */ } @Test void rejectsEmptyCart() { /* also a clean service */ } } ``` **Why per-test setup matters — test isolation.** JUnit, by default, creates a **brand-new instance of the test class for each test method**. Combined with `@BeforeEach`, this means each test starts from a known clean slate, so the *order* tests run in can't make one test pass or fail because of another. `@BeforeAll`/`@AfterAll` run once and therefore must be `static` (there's no single instance they belong to) — unless you annotate the class `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`, which keeps one instance for the whole class and lets those methods be non-static (at the cost of weaker isolation). ## Other everyday annotations - `@DisplayName("adds two positive numbers")` — a human-readable name shown in reports. - `@Disabled("flaky until JIRA-123")` — skip a test without deleting it. - `@Nested` — an inner class to group related tests. - `assertThrows(IllegalArgumentException.class, () -> calc.divide(1, 0))` — assert that calling the lambda throws the given exception type; returns the exception so you can assert on its message. ## How this maps to the language-neutral discipline The *concepts* — what a unit test is, the testing pyramid, AAA, isolation — are universal. JUnit is just Java's **realization**: `@Test` is how you mark a unit test, `@BeforeEach` is how you build the fixture, and the `Assertions` class is how you express the verify step. Other languages have equivalents (pytest, NUnit, Jest); the ideas transfer, only the syntax changes.
- Why must @BeforeAll and @AfterAll methods be static by default?Because they run once for the whole class, not per instance, and JUnit creates a new test-class instance per test method — there's no single instance to attach them to. Marking the class @TestInstance(PER_CLASS) keeps one instance and lets them be non-static.
- What's the difference between JUnit Jupiter, Platform, and Vintage?Platform is the launcher engine IDEs/build tools call; Jupiter is the JUnit 5 programming model (the annotations you write); Vintage is a compatibility engine that runs legacy JUnit 4 tests on the Platform.
Lifecycle hooks are like a theater stage crew: @BeforeAll/@AfterAll set up and strike the whole set once for the show, while @BeforeEach/@AfterEach reset the props between every individual scene so each scene starts identically.
saying these in an interview costs you the question
- Writing assertEquals(actual, expected) — argument order is expected first; reversing it produces misleading failure messages
- Sharing mutable state across tests via fields without resetting in @BeforeEach, making tests order-dependent
- Thinking JUnit 5 test methods must be public (they can be package-private)
- Confusing JUnit 4 (@Before/@After, @RunWith) annotations with JUnit 5 (@BeforeEach/@AfterEach, @ExtendWith)