Java JUnit Testing Cheat Sheet
Covers JUnit 5 test lifecycle annotations, assertions, parameterized tests, and Mockito mocking patterns for writing effective unit tests.
Basic Test Structure
Core JUnit 5 lifecycle annotations.
import org.junit.jupiter.api.*;import static org.junit.jupiter.api.Assertions.*;class CalculatorTest { private Calculator calc; @BeforeEach void setUp() { calc = new Calculator(); // Runs before each test } @Test void addsTwoNumbers() { assertEquals(5, calc.add(2, 3)); } @AfterEach void tearDown() { calc = null; // Runs after each test }}
Common Assertions
Assertion methods from org.junit.jupiter.api.Assertions.
assertEquals(expected, actual);assertEquals(expected, actual, "custom failure message");assertNotEquals(a, b);assertTrue(condition);assertFalse(condition);assertNull(obj);assertNotNull(obj);assertThrows(IllegalArgumentException.class, () -> calc.divide(1, 0));assertAll( () -> assertEquals(4, calc.add(2, 2)), () -> assertEquals(0, calc.add(0, 0)));assertArrayEquals(new int[]{1, 2}, result);assertTimeout(Duration.ofMillis(100), () -> calc.slowOp());
Parameterized Tests
Run the same test logic with multiple inputs.
import org.junit.jupiter.params.ParameterizedTest;import org.junit.jupiter.params.provider.ValueSource;import org.junit.jupiter.params.provider.CsvSource;@ParameterizedTest@ValueSource(ints = {2, 4, 6, 8})void isEven(int number) { assertEquals(0, number % 2);}@ParameterizedTest@CsvSource({"1,2,3", "2,3,5", "10,20,30"})void addsPairs(int a, int b, int expected) { assertEquals(expected, calc.add(a, b));}
Key Annotations
Lifecycle and configuration annotations in JUnit 5.
- @Test- Marks a method as a test case.
- @BeforeEach / @AfterEach- Runs before/after every test method in the class.
- @BeforeAll / @AfterAll- Runs once before/after all tests in the class; method must be static (unless using @TestInstance(PER_CLASS)).
- @Disabled- Skips a test method or class, optionally with a reason string.
- @DisplayName- Sets a custom human-readable name for a test or class in reports.
- @Nested- Groups related tests in an inner non-static class for hierarchical organization.
- @Tag- Labels tests for filtered execution (e.g. mvn test -Dgroups=slow).
- @ExtendWith- Registers an extension, e.g. @ExtendWith(MockitoExtension.class).
Mocking with Mockito
Common Mockito patterns used alongside JUnit 5.
import static org.mockito.Mockito.*;@ExtendWith(MockitoExtension.class)class OrderServiceTest { @Mock private PaymentGateway gateway; @InjectMocks private OrderService service; @Test void chargesCustomer() { when(gateway.charge(100)).thenReturn(true); boolean result = service.checkout(100); assertTrue(result); verify(gateway, times(1)).charge(100); }}
Writing a Custom Extension
Implement the JUnit 5 extension model to hook into the test lifecycle programmatically.
public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { private static final Namespace NS = Namespace.create(TimingExtension.class); @Override public void beforeTestExecution(ExtensionContext ctx) { ctx.getStore(NS).put("start", System.nanoTime()); } @Override public void afterTestExecution(ExtensionContext ctx) { long start = ctx.getStore(NS).remove("start", long.class); long tookMs = (System.nanoTime() - start) / 1_000_000; System.out.printf("%s took %dms%n", ctx.getDisplayName(), tookMs); }}@ExtendWith(TimingExtension.class)class OrderServiceTest { /* ... */ }
Dynamic Tests & @MethodSource
Generate tests at runtime and feed complex arguments from a factory method.
@TestFactoryStream<DynamicTest> pricingScenarios() { List<int[]> cases = List.of(new int[]{0, 0}, new int[]{100, 10}, new int[]{999, 99}); return cases.stream().map(c -> DynamicTest.dynamicTest("price(" + c[0] + ") == " + c[1], () -> assertEquals(c[1], PriceCalc.discount(c[0]))) );}static Stream<Arguments> discountCases() { return Stream.of( Arguments.of(0, 0), Arguments.of(100, 10), Arguments.of(999, 99) );}@ParameterizedTest@MethodSource("discountCases")void discountIsApplied(int price, int expected) { assertEquals(expected, PriceCalc.discount(price));}
Advanced JUnit 5 Annotations
Lesser-used annotations for ordering, conditional execution, and repetition.
- @TestMethodOrder- Controls execution order within a class, e.g. @TestMethodOrder(MethodOrderer.OrderAnnotation.class) with @Order(1).
- @RepeatedTest(n)- Runs the same test n times, useful for flaky/timing-sensitive or randomized-input checks; injects RepetitionInfo.
- @EnabledOnOs / @DisabledOnOs- Conditionally runs a test based on the operating system (OS.LINUX, OS.WINDOWS, OS.MAC).
- @EnabledIfEnvironmentVariable- Gates a test on an environment variable matching a regex, e.g. for CI-only integration tests.
- @TestInstance(Lifecycle.PER_CLASS)- Creates one test instance for the whole class instead of per-method, allowing non-static @BeforeAll/@AfterAll and shared instance state.
- @Timeout- Fails a test if it exceeds a duration, e.g. @Timeout(value = 500, unit = MILLISECONDS).
- @Execution(CONCURRENT)- Opts a class or method into parallel execution when the parallel engine is enabled.
Advanced Mockito: Captors, Spies, Answers
Verify argument contents, wrap real objects, and stub dynamic behavior.
@Testvoid capturesArgumentPassedToGateway() { ArgumentCaptor<PaymentRequest> captor = ArgumentCaptor.forClass(PaymentRequest.class); service.checkout(100); verify(gateway).charge(captor.capture()); assertEquals(100, captor.getValue().amount());}@Testvoid spyDelegatesToRealMethodByDefault() { List<String> spyList = spy(new ArrayList<>()); doReturn(99).when(spyList).size(); // stub only this call, real add() still runs spyList.add("a"); assertEquals(99, spyList.size());}@Testvoid answerComputesDynamicResult() { when(gateway.charge(anyInt())).thenAnswer(inv -> { int amount = inv.getArgument(0); return amount < 1000; }); assertTrue(gateway.charge(500));}@Testvoid verifiesCallOrderAcrossMocks() { InOrder inOrder = inOrder(inventory, gateway); service.checkout(100); inOrder.verify(inventory).reserve(any()); inOrder.verify(gateway).charge(100);}
Parallel Test Execution
Speed up suites by running tests concurrently via junit-platform.properties.
# src/test/resources/junit-platform.propertiesjunit.jupiter.execution.parallel.enabled = truejunit.jupiter.execution.parallel.mode.default = concurrentjunit.jupiter.execution.parallel.mode.classes.default = concurrentjunit.jupiter.execution.parallel.config.strategy = dynamicjunit.jupiter.execution.parallel.config.dynamic.factor = 2# Mark a class that must run in isolation (e.g. shares static state)# @Execution(ExecutionMode.SAME_THREAD)# @ResourceLock("shared-db") -- serialize tests touching the same external resource
Prefer assertThrows over try/catch + fail() for exception testing - it returns the exception so you can also assert on its message, and it clearly signals the intended failure path.