JUnit 5

·Cheat Sheet
JupiterPlatformVintage
Architecture
PlatformLauncher API — IDE & build tool bridge
JupiterNew API + Test Engine — what you write
VintageJUnit 3/4 backward compat engine
Maven Dependency
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-engine</artifactId>
  <version>5.11.0-M2</version>
  <scope>test</scope>
</dependency>
Spring Boot: spring-boot-starter-test already includes JUnit
Lifecycle Annotations
@BeforeAllOnce before all tests · static method
@AfterAllOnce after all tests · static method
@BeforeEachBefore every test method
@AfterEachAfter every test method
@TestMarks a test method
@DisplayName("…")Custom readable name in reports
@Disabled("reason")Skip test or class
@NestedInner non-static test class, layered setup
@Tag("name")Filter/group tests
@ExtendWith(X.class)Register custom extension
@TestInstance(PER_CLASS) → one instance for all tests in class
Do & Don't Test
✅ Test
· Business logic
· Edge cases
· Failure paths
· Exception types
· Boundary values
❌ Don't Test
· Getters / setters
· Framework code
· Private methods
· Trivial constructors
Basic Test Structure
@Test
void addsTwoNumbers() {
  Calculator c = new Calculator();
  int result = c.add(2, 3);
  assertEquals(5, result);
}
import static org.junit.jupiter.api.Assertions.*;
Assertions · org.junit.jupiter.api.Assertions
Core
assertEquals(expected, actual)
assertNotEquals(a, b)
assertTrue(condition)
assertFalse(condition)
assertNull(obj)
assertNotNull(obj)
assertArrayEquals(arr1, arr2)
assertIterableEquals(list1, list2)
Time
// same thread — waits to finish
assertTimeout(ofMillis(100), () => ...)

// different thread — kills at limit
assertTimeoutPreemptively(ofMillis(100), () => ...)
Grouped (all run even on failure)
assertAll("person",
  () => assertEquals("Jane", p.getFirst()),
  () => assertEquals("Doe", p.getLast())
);
Lambda / Lazy Message
assertTrue(numbers.stream()
  .mapToInt(Integer::intValue)
  .sum() > 5,
  () => "Sum should be > 5"  // lazy
);
No-throw
assertDoesNotThrow(() => service.process());
Optional 3rd param: String or Supplier<String> message
Failed assertion throws AssertionError
Exception Testing
@Test
void divideByZero() {
  // basic
  assertThrows(
    ArithmeticException.class,
    () => calc.divide(10, 0)
  );

  // capture for extra assertions
  ArithmeticException ex = assertThrows(
    ArithmeticException.class,
    () => calc.divide(10, 0)
  );
  assertEquals("/ by zero", ex.getMessage());
}
assertThrowsallows subclasses
assertThrowsExactlyexact type, no subclasses
assertDoesNotThrowno exception expected
Wrap in assertAll to check side effects while also catching exception
Parameterized Tests · @ParameterizedTest
@ValueSource — single arg
@ParameterizedTest
@ValueSource(ints = {1, 2, 3}) 
void isPositive(int n) {
  assertTrue(n > 0);
}
@CsvSource — multiple args
@ParameterizedTest
@CsvSource({  "1, 2, 3", "5, 7, 12"}) 
void testAdd(int a, int b, int exp) {
  assertEquals(exp, calc.add(a, b));
}
@MethodSource — factory method
@ParameterizedTest
@MethodSource("additionData")
void testAdd(int a, int b, int exp) {
  assertEquals(exp, calc.add(a, b));
}

static Stream<Arguments> additionData() {
  return Stream.of(
    Arguments.of(1, 2, 3),
    Arguments.of(5, 5, 10)
  );
}
Assumptions
Fail → TestAbortedException → test skipped (not failed)
assumeTrue(5 > 1);
assumeFalse(5 < 1);

assumingThat(
  str.equals("target"),
  () => assertEquals(4, 2+2)
);
Conditional Execution
@EnabledOnOs(OS.WINDOWS)
OS-specific
@EnabledOnJre(JRE.JAVA_17)
JRE version
@EnabledIfSystemProperty(named="k",matches="v")
System prop
@EnabledIfEnvironmentVariable(named="ENV",matches="STAGING")
Env var
@EnabledIf("condition") / @DisabledIf
Boolean expr
Skipped tests show as Skipped in reports (better than if/else)
@Nested Tests
Non-static inner classes. BDD-style test reports. Layered setup (parent → child).
class UserServiceTest {
  @BeforeEach void setup() { ... }

  @Nested
  class WhenUserExists {
    @BeforeEach void init() { ... }
    @Test void canUpdate() { ... }
  }

  @Nested
  class WhenUserMissing {
    @Test void throwsError() { ... }
  }
}
Dynamic Tests · @TestFactory
Discovered at runtime. Returns Stream/Collection/Iterable of DynamicTest. No @BeforeEach/@AfterEach per dynamic test — handle manually in lambda.
@TestFactory
Stream<DynamicTest> translateTests() {
  return inputs.stream()
    .map(word =>
      DynamicTest.dynamicTest(
        "Test: " + word,
        () => assertEquals(
          translate(word), expected)
      ));
}
Method must not be private or static
Test Suites
@Suite
@SelectClasses({AssertionTest.class,
               ExceptionTest.class})
public class AllTests {}

@Suite
@SelectPackages("com.example")
@ExcludePackages("com.example.slow")
public class FastSuite {}
Parameterized Classes
Entire class reruns per param set (vs single method with @ParameterizedTest)
@ParameterizedClass
@MethodSource("bases")
@TestInstance(Lifecycle.PER_CLASS)
class CalcTest {
  CalcTest(int base) { ... }
  @Test void addWorks() { ... }
  @Test void subWorks() { ... }
}
Meta / Composed Annotations
Jupiter annotations can be used as meta-annotations to build reusable composed annotations.
@Target({ElementType.TYPE,
        ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {}
@FastTest  // replaces @Tag("fast") + @Test
void myFastTest() { ... }
Test Instance Lifecycle
PER_METHOD (default)
New instance per test method
PER_CLASS
One instance for all methods. Allows non-static @BeforeAll
@TempDir — File Testing
JUnit creates & cleans up a temp directory automatically.
@TempDir
Path tempDir;

@Test
void shouldReadFile() throws Exception {
  File file = tempDir
    .resolve("test.txt").toFile();

  try (FileWriter w = new FileWriter(file)) {
    w.write("Hello\nWorld");
  }

  String res = util.readFile(file.getPath());
  assertEquals("Hello\nWorld\n", res);
}
File Edge Cases to Test
· Empty file
· CRLF line endings
· Unicode / special chars in filename
· Large file (10k+ lines)
· Missing file → exception
· Directory path → exception
Best Practices & Tips
TDD Flow
🔴 Red
Write failing test
🔵 Blue
Write code
🟢 Green
Refactor
Edge Cases Checklist
· Boundary / negative numbers
· All possible enum states
· Empty string & null collections
· Leap years, end-of-month, midnight (00:00:00)
· File: empty, missing, dir instead of file
CI / Build Filtering by Tag
Local
Run @Tag("fast") only
CI/CD
Run full suite incl. @Tag("integration")
· One behavior per test, use assertAll for multi-field checks
· Inject TestInfo / TestReporter as method params
· @ExtendWith to share setup across test classes
· Third-party: AssertJ, Hamcrest, Truth
JUnit 5 = Platform + Jupiter + Vintage · All annotations inherited unless overridden · Conditional skips show as Skipped in reports