Summary
JUnit is Java's premier unit testing framework, powering test-driven development with annotations, assertions, and lifecycle management. Master JUnit 5's architecture, parameterized tests, nested tests, conditional execution, and integration with mocking frameworks to write maintainable, efficient test suites that ensure code quality.
1. JUnit Basics
What is JUnit?
- Unit testing framework for Java
- Current versions: JUnit 5 (Jupiter) is latest, JUnit 4 still widely used
- Part of xUnit family of testing frameworks
JUnit 5 Architecture
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
(Foundation) (New tests) (JUnit 3/4 support)
2. Essential Annotations
JUnit 5 Annotations
@Test // Marks a test method
@BeforeEach // Runs before each test (setUp)
@AfterEach // Runs after each test (tearDown)
@BeforeAll // Runs once before all tests (static)
@AfterAll // Runs once after all tests (static)
@DisplayName("name") // Custom test name
@Disabled // Skip test
@TestInstance // Control test instance lifecycle
JUnit 4 → JUnit 5 Migration
@Before → @BeforeEach
@After → @AfterEach
@BeforeClass → @BeforeAll
@AfterClass → @AfterAll
@Ignore → @Disabled
3. Basic Test Structure
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private Calculator calc;
@BeforeEach
void setUp() {
calc = new Calculator();
}
@Test
@DisplayName("Should add two numbers correctly")
void testAddition() {
// Given
int a = 5, b = 3;
// When
int result = calc.add(a, b);
// Then
assertEquals(8, result, "5 + 3 should equal 8");
}
@AfterEach
void tearDown() {
calc = null;
}
}
4. Assertions
Common Assertions
// Basic assertions
assertEquals(expected, actual);
assertNotEquals(expected, actual);
assertTrue(condition);
assertFalse(condition);
assertNull(object);
assertNotNull(object);
// With custom message
assertEquals(5, result, "Custom failure message");
// Array assertions
assertArrayEquals(expectedArray, actualArray);
// Object reference
assertSame(expected, actual); // Same reference
assertNotSame(expected, actual);
// Grouped assertions
assertAll("person",
() -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
Exception Testing
// JUnit 5
@Test
void shouldThrowException() {
Exception exception = assertThrows(
IllegalArgumentException.class,
() -> calculator.divide(10, 0)
);
assertEquals("Cannot divide by zero", exception.getMessage());
}
// JUnit 4
@Test(expected = IllegalArgumentException.class)
public void shouldThrowException() {
calculator.divide(10, 0);
}
Timeout Testing
// JUnit 5
@Test
@Timeout(value = 2, unit = TimeUnit.SECONDS)
void timeoutTest() {
// Test fails if execution exceeds 2 seconds
}
// Or with assertTimeout
@Test
void timeoutNotExceeded() {
assertTimeout(Duration.ofSeconds(2), () -> {
// Code that should complete within 2 seconds
});
}
5. Parameterized Tests
@ParameterizedTest with Different Sources
// Value Source
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testWithValueSource(int number) {
assertTrue(number > 0 && number < 6);
}
// CSV Source
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"3, 5, 8"
})
void testAddition(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}
// Method Source
@ParameterizedTest
@MethodSource("provideTestData")
void testWithMethodSource(String input, boolean expected) {
assertEquals(expected, validator.isValid(input));
}
private static Stream<Arguments> provideTestData() {
return Stream.of(
Arguments.of("test@email.com", true),
Arguments.of("invalid.email", false)
);
}
// Enum Source
@ParameterizedTest
@EnumSource(Month.class)
void testAllMonths(Month month) {
assertNotNull(month);
}
6. Test Lifecycle & Instance
Test Instance Lifecycle
// Default: new instance per test method
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
class DefaultLifecycleTest { }
// Single instance for all tests
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SingleInstanceTest {
// @BeforeAll and @AfterAll don't need to be static
}
Test Execution Order
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedTest {
@Test
@Order(1)
void firstTest() { }
@Test
@Order(2)
void secondTest() { }
}
7. Nested Tests
@DisplayName("Stack Test")
class StackTest {
Stack<Integer> stack;
@Test
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Nested
@DisplayName("after pushing element")
class AfterPushing {
@BeforeEach
void pushElement() {
stack.push(42);
}
@Test
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
}
}
}
8. Conditional Tests
// OS-specific tests
@Test
@EnabledOnOs(OS.LINUX)
void onlyOnLinux() { }
@Test
@DisabledOnOs({OS.WINDOWS, OS.MAC})
void notOnWindowsOrMac() { }
// JRE version
@Test
@EnabledOnJre(JRE.JAVA_11)
void onlyOnJava11() { }
@Test
@EnabledForJreRange(min = JRE.JAVA_8, max = JRE.JAVA_11)
void fromJava8To11() { }
// System property
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitecture() { }
// Environment variable
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "prod")
void onlyInProduction() { }
9. Assumptions
@Test
void testOnlyOnCiServer() {
// Test only continues if assumption is true
assumeTrue("CI".equals(System.getenv("ENV")));
// Rest of test
assertEquals(2, calculator.add(1, 1));
}
@Test
void testWithAssumption() {
assumingThat("DEV".equals(System.getenv("ENV")),
() -> {
// This code only runs if assumption is true
assertEquals(2, calculator.add(1, 1));
});
// This always runs
assertNotNull(calculator);
}
10. Test Doubles (Mocking)
Using Mockito with JUnit
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void testGetUser() {
// Given
User mockUser = new User("John", "Doe");
when(userRepository.findById(1L)).thenReturn(mockUser);
// When
User result = userService.getUser(1L);
// Then
assertEquals("John", result.getFirstName());
verify(userRepository).findById(1L);
}
}
11. Test Fixtures & Resources
class FileTest {
@TempDir
Path tempDir; // JUnit creates temp directory
@Test
void testFileCreation() throws IOException {
Path file = tempDir.resolve("test.txt");
Files.writeString(file, "Hello Test");
assertTrue(Files.exists(file));
assertEquals("Hello Test", Files.readString(file));
}
}
12. Best Practices
Test Naming Conventions
// Method name describes what is being tested
@Test
void shouldReturnTrueWhenInputIsValid() { }
// Given-When-Then pattern
@Test
void givenNullInput_whenValidate_thenThrowException() { }
// Display name for readability
@Test
@DisplayName("Null input should throw IllegalArgumentException")
void testNullInput() { }
Test Structure - AAA Pattern
@Test
void testCalculation() {
// Arrange (Given)
Calculator calc = new Calculator();
int a = 5, b = 3;
// Act (When)
int result = calc.add(a, b);
// Assert (Then)
assertEquals(8, result);
}
F.I.R.S.T Principles
- Fast: Tests should run quickly
- Independent: Tests shouldn't depend on each other
- Repeatable: Same result every time
- Self-validating: Pass or fail, no manual inspection
- Timely: Written just before production code (TDD)
13. Key Interview Topics
Lifecycle Annotations Comparison
- @BeforeEach: Runs before each test method, creates fresh state
- @BeforeAll: Runs once before all tests, must be static (unless PER_CLASS)
- @AfterEach: Cleanup after each test, prevents test pollution
- @AfterAll: Final cleanup, releases shared resources
Exception Testing Strategies
// JUnit 5 - Preferred approach
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
calculator.divide(10, 0);
});
assertEquals("Cannot divide by zero", exception.getMessage());
// Test doesn't throw exception
assertDoesNotThrow(() -> calculator.divide(10, 2));
Test Management Techniques
// Disable with reason
@Disabled("Feature under development")
@Test
void futureFeatureTest() { }
// Conditional execution
@EnabledOnOs(OS.LINUX)
@DisabledIfEnvironmentVariable(named = "CI", matches = "true")
Performance Testing Approaches
// Timeout annotation
@Test
@Timeout(value = 2, unit = TimeUnit.SECONDS)
void shouldCompleteQuickly() { }
// Assertion with timeout
assertTimeout(Duration.ofMillis(100), () -> {
// Code that should complete within 100ms
});
Test-Driven Development (TDD) Cycle
- Red Phase: Write failing test that defines desired behavior
- Green Phase: Implement minimal code to pass test
- Refactor Phase: Improve code while keeping tests green
- Benefits: Better design, documentation, regression safety
14. Advanced Features
Dynamic Tests
@TestFactory
Stream<DynamicTest> dynamicTests() {
return Stream.of("A", "B", "C")
.map(str -> DynamicTest.dynamicTest(
"Test for " + str,
() -> assertNotNull(str)
));
}
Test Templates
@TestTemplate
@ExtendWith(MyTestTemplateProvider.class)
void testTemplate(String parameter) {
assertNotNull(parameter);
}
Custom Annotations
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@Tag("integration")
@Timeout(5)
public @interface IntegrationTest { }
// Usage
@IntegrationTest
void myIntegrationTest() { }
15. Testing Patterns
Test Data Builders
class UserBuilder {
private String name = "Default";
private int age = 25;
public UserBuilder withName(String name) {
this.name = name;
return this;
}
public UserBuilder withAge(int age) {
this.age = age;
return this;
}
public User build() {
return new User(name, age);
}
}
// Usage in test
User testUser = new UserBuilder()
.withName("John")
.withAge(30)
.build();
16. Integration with Build Tools
Maven Configuration
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
Gradle Configuration
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
test {
useJUnitPlatform()
}
17. Quick Reference - Assertion Methods
| Assertion | Description |
|---|---|
assertEquals(expected, actual) |
Check equality |
assertTrue(condition) |
Check if true |
assertFalse(condition) |
Check if false |
assertNull(object) |
Check if null |
assertNotNull(object) |
Check if not null |
assertThrows(exception, executable) |
Check exception thrown |
assertTimeout(duration, executable) |
Check execution time |
assertAll(executables) |
Group assertions |
fail(message) |
Fail test explicitly |
18. Common Pitfalls to Avoid
- Don't use static variables - Tests should be independent
- Don't test private methods directly - Test through public API
- Don't write overly complex tests - Keep them simple and focused
- Don't ignore failing tests - Fix or remove them
- Don't test implementation details - Test behavior, not implementation
- Don't skip assertions - Every test needs assertions
- Don't use production resources - Use test doubles/mocks
19. Interview Success Strategies
Technical Proficiency
- Version Differences: JUnit 4 vs JUnit 5 migration paths
- Lifecycle Management: Test instance lifecycle, method ordering
- Mocking Integration: Mockito, test doubles, dependency injection
- Assertion Mastery: Choose appropriate assertions for scenarios
- Test Organization: Nested classes, parameterized tests, test factories
Best Practices to Demonstrate
- Test Isolation: Each test runs independently
- Clear Naming: Descriptive test and method names
- AAA Pattern: Arrange-Act-Assert structure
- Edge Cases: Null, boundary, exceptional scenarios
- Performance: Fast execution, avoid Thread.sleep()
Communication Points
- TDD Benefits: Design improvement, living documentation
- Test Strategy: Unit vs integration vs E2E decisions
- Coverage Metrics: Line, branch, mutation testing
- Maintenance: Refactoring tests with production code
Code Quality Indicators
- FIRST Principles: Fast, Independent, Repeatable, Self-validating, Timely
- Single Responsibility: One assertion concept per test
- DRY: Avoid duplication with @BeforeEach, test utilities
- Readability: Tests as documentation
Found this useful? Pass it on.