LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 2819 min read

Unit Testing Spring Applications

Learn how to unit test Spring beans with JUnit and Mockito, and get a first look at @SpringBootTest for integration testing.

Introduction

Because Spring beans are just plain Java objects with dependencies supplied through the constructor, they are naturally easy to unit test - you do not need a running Spring container just to test a service's logic. This lesson covers testing beans with JUnit and Mockito, and previews @SpringBootTest, the annotation you will use heavily once the course moves into Spring Boot.

What You Will Learn
  • The difference between a unit test and an integration test.
  • How to unit test a plain Spring bean with JUnit.
  • How Mockito lets you mock a bean's dependencies.
  • How to test a controller in isolation from the rest of the application.
  • What @SpringBootTest is for, as a preview of Spring Boot testing.

Unit Tests vs Integration Tests

A unit test exercises one class in isolation, replacing its dependencies with test doubles (mocks or stubs) so a failure always points to that one class. An integration test runs multiple real components together - sometimes including the full Spring container and a real or in-memory database - to verify they work correctly as a whole. Both have a place; unit tests are fast and pinpoint bugs precisely, integration tests catch problems in how pieces fit together.

Unit TestIntegration Test
Spring container involved?NoUsually yes
SpeedVery fast (milliseconds)Slower (seconds)
DependenciesMockedReal (or realistic test doubles)
Best forBusiness logic correctnessWiring, configuration, end-to-end flow

Testing a Plain Bean with JUnit

Because a service bean is just a class, you can new it up directly in a test and call its methods - no Spring context required at all.

class DiscountCalculatorTest {
@Test
void appliesTenPercentDiscountOverThreshold() {
DiscountCalculator calculator = new DiscountCalculator();
double result = calculator.apply(150.0);
assertEquals(135.0, result, 0.001);
}
}

Mocking Dependencies with Mockito

When a bean depends on other beans - like a service that depends on a repository - you do not want the real repository (and a real database) involved in a unit test. Mockito lets you create a mock implementation of the dependency and control exactly what it returns.

@ExtendWith(MockitoExtension.class)
class BookServiceTest {
@Mock
private BookRepository bookRepository;
@InjectMocks
private BookService bookService;
@Test
void returnsBookWhenFound() {
Book book = new Book(1L, "Clean Code", "Robert C. Martin");
when(bookRepository.findById(1L)).thenReturn(book);
Book result = bookService.getBook(1L);
assertEquals("Clean Code", result.getTitle());
verify(bookRepository).findById(1L);
}
}
@Mock and @InjectMocks

@Mock creates a mock instance of BookRepository. @InjectMocks creates a real BookService and injects the mocks into its constructor automatically, so no Spring container is started for this test.

Testing a Controller in Isolation

A controller can be unit tested the same way - construct it directly with a mocked service, call its handler methods, and assert on the return value or the Model, without going through DispatcherServlet or an HTTP request at all.

@ExtendWith(MockitoExtension.class)
class BookControllerTest {
@Mock
private BookService bookService;
@InjectMocks
private BookController bookController;
@Test
void bookDetailsAddsBookToModel() {
when(bookService.getBook(1L)).thenReturn(new Book(1L, "Clean Code", "Robert C. Martin"));
Model model = new ConcurrentModel();
String viewName = bookController.bookDetails(1L, model);
assertEquals("book-details", viewName);
assertEquals("Clean Code", ((Book) model.getAttribute("book")).getTitle());
}
}

A Preview of @SpringBootTest

The tests above never start a Spring container, which is exactly what makes them fast unit tests. Sometimes, though, you want to verify the real wiring - that beans are configured correctly, that a controller and the actual DispatcherServlet behave correctly together, or that a query actually works against a real (or in-memory) database. @SpringBootTest, which you will use extensively once the course moves into Spring Boot, starts up a real (or trimmed-down) application context for exactly this kind of integration test.

@SpringBootTest
class BookServiceIntegrationTest {
@Autowired
private BookService bookService;
@Test
void contextLoadsAndServiceIsWired() {
assertNotNull(bookService);
}
}
Use Both, for Different Reasons

Favor plain JUnit + Mockito unit tests for business logic - they run in milliseconds and pinpoint failures precisely. Reserve @SpringBootTest for verifying that the pieces are wired together correctly, since it is noticeably slower.

Common Mistakes

Avoid These Mistakes
  • Starting a full Spring context for every test, even simple business-logic checks that do not need it.
  • Forgetting @ExtendWith(MockitoExtension.class), so @Mock and @InjectMocks fields are never actually initialized.
  • Mocking a class you own the real, fast, dependency-free version of - not everything needs a mock.
  • Not verifying interactions (verify(...)) when the test's whole point is that a dependency was called correctly.
  • Writing tests that depend on execution order or shared mutable state between test methods.

Best Practices

  • Default to plain JUnit + Mockito unit tests; reach for @SpringBootTest only when you need real wiring.
  • Keep each test focused on one behavior, with a name that describes what is being verified.
  • Mock only the direct dependencies of the class under test, not the whole dependency graph.
  • Use verify() to assert that a mock was called the way you expect, not just what it returned.
  • Keep unit tests fast - if a test suite starts taking minutes, look for tests that should not be starting a Spring context.

Frequently Asked Questions

No. A @Service is a plain Java class; you can instantiate it directly (or with @InjectMocks) and test it without any Spring container at all.

@Mock (plain Mockito) creates a standalone mock with no Spring container involved. @MockBean replaces a real bean inside a running Spring test context, which is only relevant when using @SpringBootTest or similar Spring test slices.

Generally no - test the public behavior of a class. If a private method is complex enough to need its own tests, that is often a sign it should be extracted into its own class with a public API.

Key Takeaways

  • Unit tests isolate one class and run without a Spring container; integration tests exercise real wiring together.
  • Because Spring beans are plain Java objects, they can be unit tested with plain JUnit.
  • Mockito's @Mock and @InjectMocks let you replace a bean's dependencies with controllable test doubles.
  • Controllers can be tested the same way, without going through an actual HTTP request.
  • @SpringBootTest starts a real application context for integration-style tests, and becomes central once you work with Spring Boot.

Summary

Testing is what makes it safe to keep changing a Spring application over time. With controllers, services, repositories, validation, and testing all covered, the next lesson steps back to look at common mistakes and best practices across everything you have learned so far - before the final lesson pulls it all together into one real project.

Next Lesson →

Common Mistakes & Best Practices