Unit Testing with JUnit
Unit testing is a critical part of professional software development. JUnit is the most popular testing framework for Java, enabling you to write automated, repeatable tests that verify your code works correctly.
Why Unit Testing?
- Catch bugs early: Find issues before they reach production
- Refactor with confidence: Change code knowing tests will catch regressions
- Documentation: Tests show how code is meant to be used
- Better design: Testable code is usually better designed
JUnit 5 Setup (Maven)
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
Basic Test Structure
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator(); // Fresh instance for each test
}
@Test
@DisplayName("Adding two positive numbers")
void testAddPositiveNumbers() {
assertEquals(5, calculator.add(2, 3));
}
@Test
void testAddNegativeNumbers() {
assertEquals(-5, calculator.add(-2, -3));
}
@Test
void testDivideByZero() {
assertThrows(ArithmeticException.class, () -> {
calculator.divide(10, 0);
});
}
}
Common Assertions
| Assertion | Description |
|---|---|
assertEquals(expected, actual) |
Values are equal |
assertNotEquals(a, b) |
Values are not equal |
assertTrue(condition) |
Condition is true |
assertFalse(condition) |
Condition is false |
assertNull(object) |
Object is null |
assertNotNull(object) |
Object is not null |
assertThrows(Exception.class, () -> ...) |
Code throws exception |
assertArrayEquals(arr1, arr2) |
Arrays are equal |
Lifecycle Annotations
class LifecycleTest {
@BeforeAll
static void initAll() {
// Runs once before all tests (e.g., database connection)
}
@BeforeEach
void init() {
// Runs before each test (e.g., reset state)
}
@Test
void testSomething() {
// Your test
}
@AfterEach
void tearDown() {
// Runs after each test (e.g., cleanup)
}
@AfterAll
static void tearDownAll() {
// Runs once after all tests (e.g., close connection)
}
}
Parameterized Tests
Run the same test with different inputs:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
class ParameterizedTests {
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testIsPositive(int number) {
assertTrue(number > 0);
}
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"10, 20, 30"
})
void testAdd(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}
@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void testIsBlank(String input, boolean expected) {
assertEquals(expected, StringUtils.isBlank(input));
}
static Stream<Arguments> provideStringsForIsBlank() {
return Stream.of(
Arguments.of(null, true),
Arguments.of("", true),
Arguments.of(" ", true),
Arguments.of("hello", false)
);
}
}
Testing Best Practices
AAA Pattern:
- Arrange: Set up test data and conditions
- Act: Execute the code being tested
- Assert: Verify the results
@Test
void testUserRegistration() {
// Arrange
UserService service = new UserService();
User user = new User("[email protected]", "password123");
// Act
boolean result = service.register(user);
// Assert
assertTrue(result);
assertNotNull(service.findByEmail("[email protected]"));
}
Test Naming Conventions
// Good test names describe behavior
@Test void shouldReturnTrueWhenUserIsAdmin() { }
@Test void shouldThrowExceptionWhenEmailIsInvalid() { }
@Test void givenValidUser_whenRegister_thenSuccess() { }
// Avoid vague names
@Test void test1() { } // Bad
@Test void testUser() { } // Bad
Mocking with Mockito
Mock dependencies to isolate the code under test:
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void testFindUser() {
// Arrange - define mock behavior
User mockUser = new User("[email protected]");
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// Act
User result = userService.findById(1L);
// Assert
assertEquals("[email protected]", result.getEmail());
verify(userRepository, times(1)).findById(1L);
}
}
Summary
- Use JUnit 5 for modern Java testing
- Follow AAA pattern: Arrange, Act, Assert
- Use @BeforeEach for test setup
- Use parameterized tests to reduce duplication
- Use Mockito to mock dependencies
- Write descriptive test names that explain behavior
Enjoying these tutorials?