Nested tests, @TestInstance(PER_CLASS), @BeforeAll differences between JUnit 4 and 5, conditional disabling, @ExtendWith and the extension model, timeouts, TestNG vs JUnit (and running both), the dangers of overusing mocks and @InjectMocks, verifying invocation order, mocking private methods (and why not to), and PowerMock vs Mockito.
Published September 25, 2026
The basics (@Mock, when/thenReturn, ArgumentCaptor, parameterised tests) are covered in the 2–5 years tier. Here, the focus is test design judgement: over-mocking, brittle verification, and structuring large suites with JUnit 5's features.
Short answer: Use @Nested inner (non-static) classes to group tests by scenario or state, each with its own @BeforeEach, which inherits the outer setup. Add @DisplayName to produce readable, specification-like reports:
@DisplayName("Cart")
class CartTest {
Cart cart;
@BeforeEach void newCart() { cart = new Cart(); }
@Nested @DisplayName("when empty")
class WhenEmpty {
@Test void totalIsZero() { assertThat(cart.total()).isEqualByComparingTo("0"); }
@Test void checkoutIsRejected() { assertThatThrownBy(cart::checkout).isInstanceOf(EmptyCartException.class); }
}
@Nested @DisplayName("with one item")
class WithOneItem {
@BeforeEach void addItem() { cart.add(new Item("pen", new BigDecimal("20")), 2); }
@Test void totalReflectsQuantity() { assertThat(cart.total()).isEqualByComparingTo("40"); }
@Test void removingItemEmptiesCart() { cart.remove("pen"); assertThat(cart.isEmpty()).isTrue(); }
}
}
@TestInstance(Lifecycle.PER_CLASS) for? How does @BeforeAll differ between JUnit 4 and 5?Short answer:
PER_METHOD), JUnit 5 creates a new test-class instance for every test method, which gives strong isolation. So @BeforeAll/@AfterAll must be static, like JUnit 4's @BeforeClass/@AfterClass.@TestInstance(Lifecycle.PER_CLASS): one instance for all the tests in the class, so @BeforeAll/@AfterAll can be non-static, and can use instance fields. That's useful for @Nested classes (which can't have static methods before Java 16), for expensive shared setup, and for Kotlin. The risk: state leaks between tests, so reset mutable fields in @BeforeEach.@BeforeClass was always static. The names changed too: @Before/@After became @BeforeEach/@AfterEach.Short answer: Use the built-in conditions:
@Disabled("reason") (unconditionally);@EnabledOnOs(LINUX)/@DisabledOnOs(WINDOWS);@EnabledOnJre(JAVA_21)/@EnabledForJreRange(min = JAVA_17);@EnabledIfEnvironmentVariable(named = "CI", matches = "true");@EnabledIfSystemProperty(...);@EnabledIf("customCondition") (a method returning a boolean);@EnabledInNativeImage.For runtime checks, use assumptions: assumeTrue(DockerClientFactory.instance().isDockerAvailable()) aborts, rather than fails, the test. Testcontainers offers @Testcontainers(disabledWithoutDocker = true). Spring adds @EnabledIf(expression = "${feature.enabled}", loadContext = true). Use tags (@Tag("slow")) to include or exclude groups at build time.
@ExtendWith in JUnit 5?Short answer: It registers extensions, JUnit 5's single, composable extension model, which replaced JUnit 4's @RunWith runners (only one allowed) and @Rules. Extensions hook into the lifecycle through interfaces:
BeforeAllCallback/BeforeEachCallback, and the after-callbacks;ParameterResolver (inject parameters into test methods and constructors);TestExecutionExceptionHandler;ExecutionCondition;TestInstancePostProcessor;InvocationInterceptor.Examples:
SpringExtension (included by @SpringBootTest);MockitoExtension (@Mock fields, and strict stubs);@ExtendWith(TimingExtension.class).Several extensions can be combined, and they can be registered through meta-annotations, or programmatically with @RegisterExtension fields (for configured instances).
Short answer:
@Timeout(value = 2, unit = SECONDS) on a test, class or lifecycle method: the test fails if it exceeds the limit. It can be set globally (junit.jupiter.execution.timeout.default).assertTimeout(Duration.ofMillis(500), () -> service.compute()): it runs in the same thread, and fails after completion if too slow.assertTimeoutPreemptively(...): runs the code in another thread, and aborts at the timeout. Caution: ThreadLocal-based context (Spring transactions, security) isn't available on that thread.atMost(...).Use timeouts to catch hangs (deadlocks, a missing mock that makes a call block), not as precise performance tests (use JMH for those).
Short answer:
TestNG's historical strengths:
dependsOnMethods);@BeforeSuite/@BeforeTest hooks.It's popular for Selenium or UI and end-to-end suites, and in some QA frameworks.
JUnit 5 now covers most of this: parameterised tests, tags, parallel execution, nested tests, and extensions. It's the default in the Spring ecosystem.
Choose TestNG when an existing QA framework or suite depends on it, or you need test-dependency ordering for workflow-style tests (often a smell in unit tests).
Both in one project: yes. The JUnit Platform can run TestNG tests through the TestNG engine for the JUnit Platform (and JUnit 4 through Vintage), so Maven Surefire or Gradle run everything together. Keep the conventions consistent per module, to avoid confusion.
Short answer:
verify calls and stubs mirror the code's internals, so every refactor breaks the tests even when the behaviour is unchanged (brittle tests).EntityManager, RestTemplate, AWS SDKs) is especially fragile. Use integration tests (Testcontainers, WireMock) or fakes instead.Better balance:
@InjectMocks?Short answer: @InjectMocks tries constructor, then setter, then field injection of the @Mocks, and silently leaves dependencies null if it can't match them (a type mismatch, or several constructors). That gives NPEs far from the cause, or tests that pass while half the collaborators are unset. It also:
Prefer explicit construction: var service = new OrderService(repo, payments, clock);. It's type-checked, readable, and fails to compile when the dependencies change.
Short answer: Use Mockito's InOrder:
InOrder inOrder = inOrder(inventory, payments, notifications);
inOrder.verify(inventory).reserve(order);
inOrder.verify(payments).charge(order);
inOrder.verify(notifications).sendConfirmation(order);
inOrder.verifyNoMoreInteractions();
Use it only when the order matters to the behaviour (reserve before charge, and compensation order in sagas). Otherwise, it over-specifies the implementation, and makes the tests brittle.
Short answer: Mockito can't: private methods aren't overridable, and it shouldn't be done. Private methods are implementation details, so test them through the public behaviour. If a private method is complex enough to want its own tests, extract it into a separate class (or a package-private method), and test that directly. PowerMock could stub private methods through bytecode manipulation, but it's effectively abandoned (it doesn't support Java 17+ or JUnit 5 properly). Reflection (ReflectionTestUtils.invokeMethod) can call private methods, but it also couples the tests to the internals.
Short answer:
mockStatic in try-with-resources) and constructors (mockConstruction). It's actively maintained, and works with JUnit 5 and modern Java.Migration: replace PowerMock with Mockito's inline features. Better still, refactor away the static calls (inject a Clock, wrap static utilities behind interfaces).
try (MockedStatic<UUID> uuid = mockStatic(UUID.class)) { // Mockito inline: scoped static mocking
uuid.when(UUID::randomUUID).thenReturn(FIXED);
assertThat(service.newOrderId()).isEqualTo(FIXED.toString());
}
Q: What are Mockito's strict stubs?
A: With MockitoExtension (strictness STRICT_STUBS), unused stubbings fail the test, and argument mismatches are reported clearly. That keeps tests lean, and catches wrong stubs.
Q: How do you test code that uses LocalDateTime.now()?
A: Inject a java.time.Clock (with Clock.fixed(...) in tests), and use LocalDateTime.now(clock). That avoids static mocking entirely.
Q: What's the difference between a fake, a stub, a mock and a spy? A: A stub returns canned answers. A mock is a stub that also verifies interactions. A fake is a working lightweight implementation (an in-memory repository). A spy wraps a real object, and records or partially stubs its calls.
Q: What's a good unit test naming convention?
A: Something describing the behaviour: methodUnderTest_condition_expectedOutcome, or a @DisplayName sentence like "rejects checkout when the cart is empty". Aim for tests that read like a specification.