JUnit 5 and Mockito – From Scratch to Advanced
JUnit 5 and Mockito Guide – From Scratch to Advanced (with Real-Time Spring Boot Examples)
Introduction
Imagine building a skyscraper without checking the strength of each floor. Risky, right?
That’s exactly what happens in software when we skip testing — we risk everything collapsing later.
In this guide, we’ll master JUnit 5 and Mockito from the ground up, moving from basic unit tests to advanced real-world scenarios like mocking APIs, testing exceptions, and measuring coverage with JaCoCo — all in a Spring Boot application.
By the end, you’ll have:
-
A complete testing toolkit for controllers, services, and repositories.
-
Confidence to write tests for real production scenarios.
-
Knowledge of best practices and common pitfalls to avoid.
1. Unit Testing vs Integration Testing – How JUnit Fits In
Unit Testing:
-
Tests individual components (e.g., a method in a service class).
-
Fast, isolated, no database or network calls.
-
Example: Testing a
calculateFare()
method without hitting the DB.
Integration Testing:
-
Tests multiple layers together (controller → service → repository).
-
May involve DB, APIs, message queues.
-
Example: Testing a booking API end-to-end with a real DB.
Where JUnit Fits In:
-
JUnit is a testing framework that lets you write and run repeatable tests in Java.
-
Works for both unit and integration tests.
-
Plays well with Mockito for mocking.
Mini Exercise:
-
Write down 3 examples from your project that should be tested as unit tests and 3 as integration tests.
2. JUnit 5 Features & Annotations with Examples
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage.
Common Annotations:
Annotation | Purpose |
---|---|
@Test | Marks a test method |
@BeforeEach | Runs before each test |
@AfterEach | Runs after each test |
@BeforeAll | Runs once before all tests |
@AfterAll | Runs once after all tests |
@DisplayName | Custom name for test |
@Disabled | Skips a test |
@ParameterizedTest | Runs the same test with different inputs |
class CalculatorTest {
Calculator calc;
@BeforeEach
void init() {
calc = new Calculator();
}
@Test
@DisplayName("Addition Test")
void testAdd() {
assertEquals(5, calc.add(2, 3));
}
}
3. Setting Up JUnit 5 in Spring Boot
Maven Dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
This brings:
-
JUnit 5
-
Mockito
-
Spring Test
-
AssertJ, Hamcrest
Test Folder Structure:
src
├── main
│ ├── java/com/example/demo
│ └── resources
└── test
├── java/com/example/demo
└── resources
4. Real-World Example Project – Train Booking App
We’ll test a Train Booking service with:
-
Controller
-
Service
-
Repository
-
External API for train availability
Entitry
@Entity
public class Booking {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String trainName;
private String passengerName;
private double fare;
// Constructors, getters, setters
}
Repository
public interface BookingRepository extends JpaRepository<Booking, Long> {
}
Service
@Service
public class BookingService {
@Autowired
private BookingRepository bookingRepository;
public Booking createBooking(Booking booking) {
booking.setFare(calculateFare(booking.getTrainName()));
return bookingRepository.save(booking);
}
public Booking getBooking(Long id) {
return bookingRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Booking not found"));
}
public double calculateFare(String trainName) {
return trainName.contains("AC") ? 1500.0 : 800.0;
}
}
Controller
@RestController
@RequestMapping("/bookings")
public class BookingController {
@Autowired
private BookingService bookingService;
@PostMapping
public ResponseEntity<Booking> createBooking(@RequestBody Booking booking) {
return ResponseEntity.ok(bookingService.createBooking(booking));
}
@GetMapping("/{id}")
public ResponseEntity<Booking> getBooking(@PathVariable Long id) {
return ResponseEntity.ok(bookingService.getBooking(id));
}
}
5. Writing Tests with Mockito
Service Layer Test
@ExtendWith(MockitoExtension.class)
class BookingServiceTest {
@Mock
BookingRepository bookingRepository;
@InjectMocks
BookingService bookingService;
@Test
void testCreateBooking() {
Booking booking = new Booking();
booking.setTrainName("AC Express");
when(bookingRepository.save(any(Booking.class)))
.thenReturn(new Booking(1L, "AC Express", "John", 1500.0));
Booking savedBooking = bookingService.createBooking(booking);
assertEquals(1500.0, savedBooking.getFare());
verify(bookingRepository, times(1)).save(any(Booking.class));
}
}
6. Testing Repository with H2
@DataJpaTest
class BookingRepositoryTest {
@Autowired
BookingRepository bookingRepository;
@Test
void testSaveAndFind() {
Booking booking = new Booking();
booking.setTrainName("Sleeper Express");
booking.setPassengerName("Alice");
booking.setFare(800.0);
bookingRepository.save(booking);
List<Booking> bookings = bookingRepository.findAll();
assertEquals(1, bookings.size());
}
}
7. Testing Controller with MockMvc
@WebMvcTest(BookingController.class)
class BookingControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private BookingService bookingService;
@Test
void testGetBooking() throws Exception {
Booking booking = new Booking(1L, "AC Express", "John", 1500.0);
when(bookingService.getBooking(1L)).thenReturn(booking);
mockMvc.perform(get("/bookings/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.trainName").value("AC Express"));
}
}
8. Advanced Scenarios
-
Mocking External APIs → WireMock setup
-
Testing Exceptions →
assertThrows(RuntimeException.class, ...)
-
Parameterized Tests →
@CsvSource({"AC Express,1500.0", "Sleeper Express,800.0"})
-
Verifying Calls →
verify(bookingRepository, times(1)).save(any())
9. Best Practices
-
Test method naming:
methodName_expectedBehavior_condition
-
Keep tests independent
-
Avoid over-mocking
-
Target 80%+ coverage but don’t write useless tests
10. JaCoCo Setup
Maven Plugin:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
Run:
mvn clean test
Report will be in:
target/site/jacoco/index.html
Conclusion
Testing isn’t just about finding bugs — it’s about building confidence in your code.
With JUnit 5 and Mockito, you can write clean, fast, reliable tests for any Spring Boot app.