JUnit (Unit Testing)
Writing tests with JUnit 5: the annotations, lifecycle callbacks, assertions, parameterized tests, nesting, and the test instance lifecycle. Interviewers use it as the baseline check that you actually write tests rather than talk about them.
part ofJavaoverview, primer and where to startread it →on this pageshowhide
explore
- JUnit 4 vs JUnit 55 questions
- @Test5 questions
- @BeforeEach / @AfterEach5 questions
- @BeforeAll / @AfterAll5 questions
- JUnit Assertions6 questions
- assertThrows5 questions
- assertAll4 questions
- @ParameterizedTest6 questions
- @DisplayName5 questions
- @Disabled5 questions
- @Nested4 questions
- Assumptions4 questions
- Test Lifecycle (per-method vs per-class)5 questions
questions
64 · 13 sectionsWhat are the main annotation differences between JUnit 4 and JUnit 5, and how would you migrate a simple test class?
basics
~10 sJUnit 5 renames the lifecycle annotations: @Before becomes @BeforeEach, @After becomes @AfterEach, @BeforeClass becomes @BeforeAll, @AfterClass becomes @AfterEach's partner @AfterAll, and @Ignore becomes @Disabled. They also move to the org.junit.jupiter.api package.
Describe the three-part architecture of JUnit 5 (Platform, Jupiter, Vintage) and why it was split that way.
basics
~20 sJUnit 5 is three pieces: the Platform (launches tests and lets build tools/IDEs run them), Jupiter (the new way to write and run tests), and Vintage (an engine that runs your old JUnit 4 tests). JUnit 4 was a single library with no such separation.
How do JUnit 5 assertions and exception testing differ from JUnit 4's, including the assertion message and expected-exception handling?
basics
~20 sJUnit 5 assertions live in org.junit.jupiter.api.Assertions, the optional message moved to the last argument (it was first in JUnit 4), and you test exceptions with assertThrows(...) instead of @Test(expected=...). JUnit 5 also adds assertAll and assertThrows returning the caught exception.
How does JUnit 5's extension model (@ExtendWith) differ from JUnit 4's @RunWith and Rules, and why is it considered an improvement?
basics
~20 sJUnit 4 customized test behavior with @RunWith (only one runner per class) and @Rule objects. JUnit 5 replaces both with extensions registered via @ExtendWith, and you can apply as many as you want, composing them freely.
You inherit a large codebase with thousands of JUnit 4 tests. How do you adopt JUnit 5 incrementally, and what are the trade-offs of the Vintage bridge?
basics
~20 sAdd the JUnit 5 dependencies including the Vintage engine, so old JUnit 4 tests keep running while you write all new tests in JUnit 5. Then migrate old tests gradually, removing Vintage once they're all converted.
What does the @Test annotation do in JUnit 5, and what are the signature requirements for a method marked with it?
basics
~10 s@Test marks a method as a test case so JUnit runs it. The method must return void and take no parameters (unless something is injected), and should not be static or private.
JUnit 4 let you write @Test(expected = SomeException.class) and @Test(timeout = 1000). How do you express those in JUnit 5, and why was the change made?
basics
~10 sJUnit 5's @Test has no expected or timeout attributes. Instead you use assertThrows to check an exception is thrown and assertTimeout (or @Timeout) to bound how long code may run.
A developer writes a method marked @Test but it never runs — no pass, no fail, no error. What are the common reasons a @Test method is silently not discovered or executed by JUnit 5?
basics
~20 sUsual causes: importing JUnit 4's org.junit.Test instead of org.junit.jupiter.api.Test, the method being private or static, the test class or method matching no naming/include pattern in the build config, or the JUnit Platform/Jupiter engine not being on the runtime classpath.
How does the JUnit 5 Jupiter engine discover and execute methods annotated with @Test, and where does @Test fit within the JUnit 5 architecture?
basics
~20 sJUnit 5 has a Platform that launches tests, a Jupiter engine that finds @Test methods by scanning classes via reflection, and the API (@Test) you annotate with. The engine builds a test plan, creates an instance per test, runs lifecycle callbacks, then invokes each @Test method.
Why does JUnit 5's @Test have no attributes at all, and how does that decision interact with composed (meta-)annotations and the extension model?
basics
~20 sJUnit 5 keeps @Test a bare marker and moves behavior (exceptions, timeouts, conditions) into separate assertions, annotations, and extensions. That makes @Test composable: you can build your own annotations that combine @Test with tags or extensions, and add cross-cutting behavior without changing @Test itself.
What do @BeforeEach and @AfterEach do in JUnit 5, and why would you use them?
basics
~20 s@BeforeEach runs before every test method and @AfterEach runs after each one. You use them to set up a fresh starting state (like creating objects) before each test and to clean up afterward, so tests don't affect each other.
What is the difference between @BeforeEach/@AfterEach and @BeforeAll/@AfterAll in JUnit 5?
basics
~20 s@BeforeEach/@AfterEach run before and after every single test. @BeforeAll/@AfterAll run only once for the whole test class — before any test and after all tests. The 'Each' ones are for per-test setup; the 'All' ones are for expensive shared setup.
When a JUnit 5 test class extends a base test class, in what order do the @BeforeEach and @AfterEach methods of the superclass and subclass execute?
basics
~10 sSuperclass @BeforeEach methods run before subclass @BeforeEach methods, and subclass @AfterEach methods run before superclass @AfterEach methods. It nests like constructors: setup goes top-down (parent first), cleanup goes bottom-up (child first).
Why is @BeforeEach the standard way to achieve test isolation, and what are the main pitfalls of relying on @BeforeAll for shared mutable state?
basics
~20 s@BeforeEach rebuilds the test's objects before every test, so no test sees leftovers from another — tests stay independent and can run in any order. @BeforeAll runs once and any mutable state it creates is shared, so one test can corrupt it for the next, causing order-dependent, flaky failures.
In JUnit 5, when should you initialize test state in a @BeforeEach method versus in the test class's constructor or field initializers?
basics
~20 sBecause JUnit makes a new test-class instance for each test, the constructor and field initializers also run before every test, so simple setup works there too. Prefer @BeforeEach when setup needs framework features (like injected parameters) or should run as the documented lifecycle step; use the constructor for plain field assignment.
What do @BeforeAll and @AfterAll do in JUnit 5, and how do they differ from @BeforeEach and @AfterEach?
basics
~10 s@BeforeAll runs once before all the tests in a class; @AfterAll runs once after all of them. @BeforeEach and @AfterEach run before and after every single test method instead.
How do you decide whether a piece of setup belongs in @BeforeAll or @BeforeEach? What's the trade-off?
basics
~20 sPut setup in @BeforeAll if it's expensive and the same for every test and safe to share (it runs once). Put it in @BeforeEach if each test needs its own fresh copy (it runs every time). The trade-off is speed (BeforeAll) versus isolation (BeforeEach).
Why must @BeforeAll and @AfterAll methods be static under JUnit 5's default lifecycle?
basics
~20 sBy default JUnit 5 creates a new instance of the test class for every test method. @BeforeAll/@AfterAll run once, before any instance exists, so they can't belong to an instance — they must be static (belong to the class itself).
When a test class inherits from a base class with its own @BeforeAll, and there are multiple @BeforeAll methods, in what order does JUnit 5 run them — and how does @AfterAll mirror that?
basics
~20 sInherited @BeforeAll methods from a superclass run before the subclass's @BeforeAll; @AfterAll runs in the reverse order (subclass first, then superclass). Multiple @BeforeAll methods in the same class run in an unspecified order unless you set one with @Order.
In JUnit 5's assertEquals, what is the argument order, and why does getting it wrong matter?
basics
~20 sThe order is assertEquals(expected, actual). The first value is what you expect, the second is what your code produced. Swapping them still passes or fails correctly, but the failure message reads backwards and confuses you.
How do assertArrayEquals and assertIterableEquals compare their inputs, and how does that differ from calling assertEquals on a List or array?
basics
~20 sassertArrayEquals compares two arrays element by element (in order). assertIterableEquals walks two iterables (like Lists) and compares elements pairwise. assertEquals on raw arrays compares references, not contents, so it usually fails even when contents match.
What is the difference between assertEquals and assertSame in JUnit 5, and when would you use each?
basics
~20 sassertEquals checks that two objects are equal by value (using .equals()). assertSame checks they are the exact same object in memory (using ==). Use assertSame only when identity matters, like verifying a cache returns the same instance.
How do you correctly assert equality of floating-point values in JUnit 5, and why is a plain assertEquals on doubles dangerous?
basics
~10 sUse the overload assertEquals(expected, actual, delta), where delta is a small tolerance. Floating-point math has rounding errors, so 0.1 + 0.2 isn't exactly 0.3. The delta says 'close enough'.
JUnit 5 assertions accept an optional failure message — what forms can it take, and why is the Supplier form preferred for expensive messages?
basics
~20 sYou can pass a String message as the last argument, shown only when the assertion fails. JUnit also accepts a Supplier<String> lambda; the lambda runs only on failure, so a costly message isn't built every time the test passes.
What is JUnit 5's assertThrows and how do you use it to verify that code throws an expected exception?
basics
~20 sassertThrows runs a piece of code and checks it throws the exception type you expect. You pass the expected exception class and a lambda holding the code. The test passes only if that code throws that type (or a subtype).
How do you assert on the message and cause of an exception captured by assertThrows, and what makes such assertions robust rather than brittle?
basics
~20 sassertThrows returns the exception, so you store it in a variable and call getMessage() or getCause() on it, then assert with normal assertEquals/assertTrue. To stay robust, check a stable substring or the cause's type rather than the entire exact message text.
What is the difference between assertThrows and assertThrowsExactly in JUnit 5?
basics
~20 sassertThrows passes if the code throws the expected type or any subclass of it. assertThrowsExactly passes only if the thrown exception is exactly that class — a subclass fails. Use exactly when the precise type matters.
Why did JUnit 5's assertThrows replace JUnit 4's @Test(expected=...) and the ExpectedException rule?
basics
~20 s@Test(expected=...) only checked the type and let any line in the test throw it, so it was imprecise and couldn't check the message easily. ExpectedException needed setup before the call. assertThrows scopes the check to exact lines and returns the exception to inspect.
What are the common pitfalls when using assertThrows, including swallowed exceptions, multiple statements in the executable, and using it where exception testing isn't the goal?
basics
~20 sDon't put many lines in the lambda — an earlier line could throw the expected type and make the test pass for the wrong reason. Wrap only the call you expect to throw. And don't use assertThrows just to silence a checked exception in a test.
What is JUnit 5's assertAll, and what problem does it solve compared to writing several separate assertions?
basics
~20 sassertAll runs a group of assertions together and reports every failure at once. With plain asserts, the test stops at the first failure, so you only see one problem. assertAll shows all of them in one run.
Why must each assertion inside assertAll be wrapped in a lambda (Executable), and what goes wrong if you call the assertions directly?
basics
~20 sEach assertion is a lambda so assertAll can decide when to run it and catch its failure. If you called the asserts directly, the first failing one would throw right away and the rest would never run — defeating the point.
When should you prefer grouped assertions with assertAll over independent assert calls, and when is assertAll the wrong choice?
basics
~20 sUse assertAll when you check several independent properties of one thing and want to see every failure at once. Avoid it when a later check depends on an earlier one passing, because assertAll runs them all even after a failure.
How does assertAll behave with nested assertAll groups, and what are the precise semantics of its single-vs-multiple-failure reporting?
basics
~20 sYou can nest an assertAll inside another. If only one check fails, assertAll rethrows that exact failure; if several fail, it throws one MultipleFailuresError bundling them. Nesting groups related checks and keeps the failure tree organized.
What is @ParameterizedTest in JUnit 5, and how does it differ from a plain @Test?
basics
~20 s@ParameterizedTest runs the same test method many times, once per set of input values. A plain @Test runs once with no arguments. You pair it with a source annotation (like @ValueSource) that supplies the values.
What is @MethodSource, when is it the right choice, and what are the rules for the factory method it points to?
basics
~20 s@MethodSource names a method that returns the test data, usually a Stream of Arguments. Use it when the inputs are real objects or computed at runtime — things you can't write as plain literals in @ValueSource or @CsvSource.
Compare @ValueSource and @CsvSource. When would you choose each, and how do they map to test method parameters?
basics
~10 s@ValueSource gives a single column of values to a one-parameter method. @CsvSource gives comma-separated rows, so each row maps to several parameters. Use @ValueSource for one input; use @CsvSource when you need input-plus-expected pairs.
How do @EnumSource and the @ParameterizedTest name template work, including filtering enum constants and customizing display names?
basics
~20 s@EnumSource runs the test once per constant of an enum, injecting each constant. You can include or exclude specific names with its mode/names attributes. The name attribute on @ParameterizedTest sets each invocation's display label using placeholders like {index} and {0}.
Explain implicit and explicit argument conversion in @ParameterizedTest, and when you would use @ConvertWith or aggregate arguments with @AggregateWith / ArgumentsAccessor.
basics
~20 sJUnit automatically converts string source values (like from @CsvSource) into the parameter's type — that's implicit conversion. When the built-in rules can't do it, you supply a converter with @ConvertWith. To bundle many columns into one object, you use an ArgumentsAccessor or a custom aggregator via @AggregateWith.
What is the @DisplayName annotation in JUnit 5 and why would you use it?
basics
~20 s@DisplayName is a JUnit 5 annotation that gives a test method or class a custom, human-readable name shown in reports and IDEs instead of the method name. It can contain spaces, punctuation, and even emojis.
What is a DisplayNameGenerator and how do you apply one to derive readable test names automatically?
basics
~10 sA DisplayNameGenerator is a JUnit 5 strategy that builds a test's display name from its class/method automatically. You attach one with @DisplayNameGeneration so you do not have to write @DisplayName on every test.
For parameterized and repeated tests, how do per-invocation names relate to @DisplayName, and what controls them?
basics
~10 s@DisplayName names the whole parameterized or repeated test. Each individual invocation gets its own name from the test's name pattern (the name attribute), not from @DisplayName.
How do @Nested classes combine with @DisplayName and the IndicativeSentences generator to produce readable, hierarchical test reports?
basics
~10 s@Nested groups related tests into inner classes, each able to carry its own @DisplayName, so reports read as a tree. IndicativeSentences joins the enclosing names with the method name into one readable sentence.
When standardizing test naming across a large codebase, how would you decide between explicit @DisplayName, a DisplayNameGenerator default, and method-name conventions?
basics
~20 sPick one default strategy for the whole codebase (often a generator like ReplaceUnderscores set via the config property), allow explicit @DisplayName to override where wording matters, and enforce it so reports stay consistent and low-maintenance.
What does the @Disabled annotation do in JUnit 5, and what is the recommended way to use it?
basics
~20 s@Disabled tells JUnit to skip a test method or whole test class instead of running it. You should always pass a reason string, like @Disabled("flaky on CI - see TICKET-123"), so it is clear why it is off.
Why does @Disabled take an optional reason string, and what is the cost of omitting it?
basics
~10 sThe reason string explains why the test is turned off. JUnit prints it in the report. Without it, nobody knows why the test is disabled or when it is safe to turn back on.
Walk through JUnit 5's conditional execution annotations - @EnabledOnOs, @EnabledIfSystemProperty, @EnabledIfEnvironmentVariable, @EnabledIf - and how each decides whether a test runs.
basics
~20 sThese annotations turn a test on or off based on a condition: the operating system (@EnabledOnOs), a JVM system property (@EnabledIfSystemProperty), an OS environment variable (@EnabledIfEnvironmentVariable), or a custom boolean method (@EnabledIf). Each has a @Disabled... twin that does the opposite.
When should you use a conditional annotation like @DisabledOnOs or @EnabledOnOs instead of an unconditional @Disabled?
basics
~20 sUse @Disabled when a test is just broken or parked. Use conditional annotations like @EnabledOnOs(WINDOWS) or @DisabledOnOs(MAC) when the test should run only under a real condition - a specific OS, Java version, or environment - so it re-enables itself automatically when that condition holds.
How does a disabled test interact with lifecycle callbacks, fixtures, and test reporting, and what risks come with accumulating disabled tests at scale?
basics
~20 sA disabled test is skipped entirely - its body and its @BeforeEach/@AfterEach for that test do not run, though class-level setup may still happen. It shows as 'skipped', not passed. Lots of disabled tests quietly remove coverage, so track and prune them.
How do lifecycle methods like @BeforeEach and @AfterEach stack and execute across nesting levels in JUnit 5?
basics
~20 sOuter @BeforeEach methods run before inner ones, going from the outermost class inward, then the test runs, then @AfterEach methods run in reverse order — innermost first, outermost last. Each level layers its own setup and teardown around the test.
What is JUnit 5's @Nested annotation, and why would you use it to organize your tests?
basics
~20 s@Nested marks an inner test class inside another test class. It groups related tests together so you can express scenarios like 'when the cart is empty' as their own block with their own setup, making tests easier to read and organize.
When does @Nested improve a test suite versus harm it, and what design trade-offs guide how deeply you nest?
basics
~20 sUse @Nested when tests fall into clear scenarios that share setup, so the report reads like sentences and duplication drops. Avoid nesting so deeply that setup becomes hard to follow or scenarios get artificial. Keep nesting shallow and meaningful.
In JUnit 5, what is the difference between an assumption (assumeTrue) and an assertion (assertTrue)? What happens to a test when each one fails?
basics
~20 sAn assertion checks the result you are testing: if it is false, the test FAILS. An assumption checks a precondition: if it is false, the test is SKIPPED (aborted), not failed. So a failed assumption is treated like a test that did not run.
Walk through the assumeTrue and assumeFalse APIs in JUnit 5. What overloads exist (boolean vs BooleanSupplier, custom message), and what is a common use case?
basics
~20 sassumeTrue(condition) keeps running the test only if the condition is true; assumeFalse(condition) keeps running only if it is false. Otherwise the test is skipped. Both can take a message, and both have a version that takes a BooleanSupplier so the condition is only evaluated lazily.
How does assumingThat differ from assumeTrue in JUnit 5? Explain its two-argument form and when you would reach for it.
basics
~20 sassumeTrue aborts the whole test when its condition is false. assumingThat takes a condition AND a block of code (an Executable): it runs that block only if the condition is true, and otherwise just skips the block and lets the rest of the test continue normally. It does not abort the test.
What are the consequences for test reporting, lifecycle callbacks, and coverage metrics when tests are aborted by assumptions? Why can heavy use of assumptions be risky at scale?
basics
~20 sAn aborted test counts as 'skipped', not 'passed' and not 'failed'. Lifecycle teardown still runs, but the test's body did not. Skipped tests do not exercise your code, so they contribute nothing to coverage. If many tests silently skip in CI, you can have a green build that actually tested very little.
What is the default JUnit 5 test instance lifecycle, and why does JUnit create a brand-new instance of the test class for each @Test method?
basics
~20 sBy default JUnit 5 makes a fresh copy of your test class for every @Test method (PER_METHOD). This keeps tests isolated: fields set by one test can't leak into another, so test order doesn't matter.
Walk through the exact order in which JUnit 5 invokes constructor, @BeforeAll, @BeforeEach, @Test, @AfterEach, and @AfterAll for a two-test class — and how that sequence differs between PER_METHOD and PER_CLASS.
basics
~20 s@BeforeAll runs once at the start, @AfterAll once at the end. Around every @Test, JUnit runs the constructor then @BeforeEach before, and @AfterEach after. In PER_METHOD the constructor runs once per test; in PER_CLASS it runs only once.
What does @TestInstance(Lifecycle.PER_CLASS) change about how JUnit 5 runs a test class, and what does it enable?
basics
~10 s@TestInstance(Lifecycle.PER_CLASS) tells JUnit to create just one instance of the test class and reuse it for every @Test. Because there's now a shared instance, @BeforeAll and @AfterAll can be non-static methods.
Why must @BeforeAll/@AfterAll be static under the default lifecycle, and what subtle bugs arise from mixing static fixture state with per-method test instances?
basics
~20 sUnder the default PER_METHOD lifecycle JUnit makes a new instance per test, so there's no single instance to run a once-per-class method on — hence @BeforeAll/@AfterAll must be static (called on the class). The bug risk: static fixture fields are shared across all tests and aren't reset, so mutations leak between tests.