Now Reading: Unit testing Spring MVC applications with JUnit 5

Loading
svg

Unit testing Spring MVC applications with JUnit 5

NewsOctober 30, 2025Artifice Prime
svg8

Spring is a reliable and popular framework for building web and enterprise Java applications. In this article, you’ll learn how to unit test each layer of a Spring MVC application, using built-in testing tools from JUnit 5 and Spring to mock each component’s dependencies. In addition to unit testing with MockMvc, Mockito, and Spring’s TestEntityManager, I’ll also briefly introduce slice testing using the @WebMvcTest and @DataJpaTest annotations, used to optimize unit tests on web controllers and databases.

Also see: How to test your Java applications with JUnit 5.

Overview of testing Spring MVC applications

Spring MVC applications are defined using three technology layers:

  • Controllers accept web requests and return web responses.
  • Services implement the application’s business logic.
  • Repositories persist data to and from your back-end SQL or NoSQL database.

When we unit test Spring MVC applications, we test each layer separately from the others. We create mock implementations, typically using Mockito, for each layer’s dependencies, then we simulate the logic we want to test. For example, a controller may call a service to retrieve a list of objects. When testing the controller, we create a mock service that either returns the list of objects, returns an empty list, or throws an exception. This test ensures the controller behaves correctly.

We’ll use Spring MVC to build and test a simple web service that manages widgets. The structure of the web service is shown here:

Diagram of a Spring MVC web service application.

Steven Haines

This is a classic MVC pattern. We have a widget controller that handles RESTful requests and delegates its business functionality to a widget service, which uses a widget repository to persist widgets to and from an in-memory H2 database.

Get the source: Download the source code for this article.

Unit testing a Spring MVC controller with MockMvc

Setting up a Spring MVC controller test is a two-step process:

  • Annotate your test class with @WebMvcTest.
  • Autowire a MockMvc instance into your controller.

We could annotate all our test classes with @SpringBootTest, but we’ll use @WebMvcTest instead. The reason is that the @WebMvcTest annotation is used for slice testing. Whereas @SpringBootTest loads your entire Spring application context, @WebMvcTest loads only your web-related resources. Furthermore, if you specify a controller class in the annotation, it will only load the specific controller you want to test. Testing a single “slice” of your application reduces both the amount of compute resources required to set up the test and the time required to run a test.

For example, when we test a controller, we’ll mock just the services it uses, and we won’t need any repositories at all. If we don’t need them, then we needn’t waste time loading them. Slice tests were created to make tests perform better and run faster.

Here’s the source code for the Widget class we’ll be managing:

package com.infoworld.widgetservice.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Widget {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;
    private int version;

    public Widget() {
    }

    public Widget(String name) {
        this.name = name;
    }

    public Widget(String name, int version) {
        this.name = name;
        this.version = version;
    }

    public Widget(Long id, String name, int version) {
        this.id = id;
        this.name = name;
        this.version = version;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getVersion() {
        return version;
    }

    public void setVersion(int version) {
        this.version = version;
    }
}

A Widget is a JPA entity that manages three fields:

  • id is the primary key of the table, annotated with @Id and @GeneratedValue, with an automatic generation strategy.
  • name is the name of the widget.
  • version is the version of the widget resource. We’ll use this value to populate our eTag value and check it in our PUT operation’s If-Match header value. This ensures the widget being updated is not stale.

Here’s the source code for the controller we’ll be testing (WidgetController.java):

package com.infoworld.widgetservice.web;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Optional;
import com.infoworld.widgetservice.model.Widget;
import com.infoworld.widgetservice.service.WidgetService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WidgetController {
    @Autowired
    private WidgetService widgetService;
    @GetMapping("/widget/{id}")
    public ResponseEntity getWidget(@PathVariable Long id) {
        return widgetService.findById(id)
                .map(widget -> {
                    try {
                        return ResponseEntity
                                .ok()
                                .location(new URI("/widget/" + id))
                                .eTag(Integer.toString(
                                               widget.getVersion()))
                                .body(widget);
                    } catch (URISyntaxException e) {
                        return ResponseEntity
                          .status(HttpStatus.INTERNAL_SERVER_ERROR)
                          .build();
                    }
                })
                .orElse(ResponseEntity.notFound().build());
    }
    @GetMapping("/widgets")
    public List getWidgets() {
        return widgetService.findAll();
    }
    @PostMapping("/widgets")
    public ResponseEntity createWidget(@RequestBody Widget widget)
    {
        Widget newWidget = widgetService.create(widget);
        try {
           return ResponseEntity
                   .created(new URI("/widget/" + newWidget.getId()))
                   .eTag(Integer.toString(newWidget.getVersion()))
                   .body(newWidget);
        } catch (URISyntaxException e) {
            return ResponseEntity
                    .status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .build();
        }
    }

    @PutMapping("/widget/{id}")
    public ResponseEntity updateWidget(@PathVariable Long id,
                                          @RequestBody Widget widget,
                         @RequestHeader("If-Match") Integer ifMatch) {
        Optional existingWidget = widgetService.findById(id);
        return existingWidget.map(w -> {
            if (w.getVersion() != ifMatch) {
                return ResponseEntity.status(HttpStatus.CONFLICT)
                                     .build();
            }

            w.setName(widget.getName());
            w.setVersion(w.getVersion() + 1);

            Widget updatedWidget = widgetService.save(w);
            try {
                return ResponseEntity.ok()
                        .location(new URI("/widget/" + 
                                      updatedWidget.getId()))
                        .eTag(Integer.toString(
                                      updatedWidget.getVersion()))
                        .body(updatedWidget);
            } catch (URISyntaxException e) {
                throw new RuntimeException(e);
            }
        }).orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("widget/{id}")
    public ResponseEntity deleteWidget(@PathVariable Long id) {
        Optional existingWidget = widgetService.findById(id);
        return existingWidget.map(w -> {
           widgetService.deleteById(w.getId());
           return ResponseEntity.ok().build();
        }).orElse(ResponseEntity.notFound().build());
    }
}

The WidgetController handles GET, POST, PUT, and DELETE operations, following standard RESTful principles, so we’re going to write tests for each operation.

The following source code shows the structure of our test class (WidgetControllerTest.java):

package com.infoworld.widgetservice.web;

@WebMvcTest(WidgetController.class)
public class WidgetControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private WidgetService widgetService;
}

I omitted the imports for readability, but the important thing to note is that the class is annotated with the @WebMvcTest annotation, and that we pass in the WidgetController.class as the controller we’re testing. This tells Spring to only load the WidgetController and no other Spring resources. The @WebMvcTest annotation includes other annotations, but the important one for our tests is @AutoConfigureMockMvc, which will cause Spring to create a MockMvc instance and add it to the application context. That lets us autowire it into our test class using the @Autowired annotation.

Next, we use the @MockitoBean annotation to use Mockito to create a mock implementation of the WidgetService, after which Spring will autowire it into the WidgetController class. This lets us control the behavior of the WidgetService for the WidgetController test cases we’re writing. Note that starting in Spring Boot version 3.4, @MockitoBean replaced @MockBean. Everything you know about @MockBean translates to using @MockitoBean—with some improvements.

Unit testing GET /widgets

Let’s start with the easiest test case, a test for GET /widgets:

@Test
void testGetWidgets() throws Exception {
    List widgets = new ArrayList<>();
    widgets.add(new Widget(1L, "Widget 1", 1));
    widgets.add(new Widget(2L, "Widget 2", 1));
    widgets.add(new Widget(3L, "Widget 3", 1));

    when(widgetService.findAll()).thenReturn(widgets);

    mockMvc.perform(get("/widgets"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.length()").value(3))
            .andExpect(jsonPath("$[0].id").value(1L))
            .andExpect(jsonPath("$[0].name").value("Widget 1"))
            .andExpect(jsonPath("$[0].version").value(1));
};

The testGetWidgets() method creates a list of three widgets and then configures the mock WidgetService to return the list when its findAll() method is called. The WidgetControllerTest class statically imports the org.mockito.Mockito.when() method that accepts a method call, which in this case is widgetService.findAll(), and returns a Mockito OngoingStubbing instance. This OngoingStubbing instance exposes methods like thenReturn(), thenThrow(), thenCallRealMethod(), thenAnswer(), and then().

Here, we use the thenReturn() method to tell Mockito to return the list of widgets when the WidgetService’s findAll() method is called. The @MockitoBean annotation causes the mock WidgetService to be autowired into the WidgetController. So, when the getWidgets() method is called in response to a GET /widgets, it calls the WidgetService’s findAll() method and returns our list of widgets as a web response.

Next, we use MockMvc’s perform() method to execute a web request. This diagram shows the various classes that interact with the perform() method:

Diagram of classes that interact with the MockMvc perform() method.

Steven Haines

The perform() method accepts a RequestBuilder. Spring defines several built-in RequestBuilders that we can statically import into our tests, including get(), post(), put(), and delete(). The perform() method returns a ResultActions instance that exposes methods such as andExpect(), andExpectAll(), andDo(), and andReturn(). Here, we invoke the andExpect() method, which accepts a ResultMatcher.

A ResultMatcher defines a match() method that throws an AssertionError if the assertion fails. Spring defines several ResultMatchers that we can statically import:

  • status() allows us to check the HTTP status code of response.
  • content() allows us to check the content headers of the response, such as Content-Type.
  • header() allows us to check any of the HTTP header values.
  • jsonPath() allows us to inspect the contents of a JSON document.

After MockMvc performs a GET to /widgets, we expect the HTTP status code to be 200 OK. We can then use the jsonPath matcher to check the body results, using the following JSON path expressions:

  • $.length(): The $ references the root of the JSON document. If the response is a list, then we can call the length() method to get the number of elements in the list.
  • $[0].id: JSON path expressions for a list use an array syntax starting at 0. This expression gets the ID of the first element in the list.
  • $[0].name: This expression gets the name of the first element and compares it to “Widget 1”.
  • $[0].version: This expression gets the version of the first element and compares it to 1.

Unit testing the GET /widget/{id} handler

Here’s the source code to test the GET /coffee/{id} widget:

@Test
void testGetWidgetById() throws Exception {
    Widget widget = new Widget(1L, "My Widget", 1);          
    when(widgetService.findById(1L))
           .thenReturn(Optional.of(widget));

    mockMvc.perform(get("/widget/{id}", 1))
            // Validate that we get a 200 OK Response Code
            .andExpect(status().isOk())

            // Validate Headers
            .andExpect(content()
                      .contentType(MediaType.APPLICATION_JSON))
            .andExpect(header().string(HttpHeaders.LOCATION,
                                       "/widget/1"))
            .andExpect(header().string(HttpHeaders.ETAG, "\"1\""))

            // Validate content
            .andExpect(jsonPath("$.id").value(1L))
            .andExpect(jsonPath("$.name").value("My Widget"))
            .andExpect(jsonPath("$.version").value(1));
 }

This test method is very similar to the testGetWidgets() method, but with some notable changes:

  • The GET URI is defined using a URI template. You can specify any number of variables enclosed in braces in the URI template and then send a list of arguments that will replace those variables in the order they appear in the template.
  • We check that the returned Content-Type is “application/json”, which is a constant in the MediaType class. We access the content using the content() method, which returns a ContentResultMatchers instance that provides various methods, including contentType(), which allows us to validate the content headers.
  • We check for specific header values using the header() method. The header() method returns a HeadersResultMatchers instance, which can check for header String, long, and date values, as well as checking to see whether or not specific headers exist. In this case, we use constants defined in the HttpHeaders class to check the location and eTag header values.
  • We check the body of the response using JSON path expressions. In this case, we do not have a list of objects, so we can access the individual fields in the JSON document directly. For example, $.id retrieves the id field value in the root of the document.

Unit testing a GET /widget/{id} Not Found code

Next, we test the GET /widget/{id}, passing it an invalid ID so that it returns a 404 Not Found response code:

@Test
void testGetWidgetByIdNotFound() throws Exception {
   when(widgetService.findById(1L)).thenReturn(Optional.empty());

   mockMvc.perform(get("/widget/{id}", 1))
            // Validate that we get a 404 Not Found Response Code
            .andExpect(status().isNotFound());
}

The testGetWidgetByIdNotFound() method configures the mock WidgetService to return Optional.empty() when its findById() is called with a value of 1. We then perform a GET request to /widget/1, then assert that the returned HTTP status code is 404 Not Found.

Unit testing POST /widgets

Here’s how to test a Widget creation:

@Test
void testCreateWidget() throws Exception {
    Widget widget = new Widget(1L, "Widget 1", 1);
    when(widgetService.create(any())).thenReturn(widget);

    mockMvc.perform(post("/widgets")
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"name\": \"Widget 1\"}"))

            // Validate that we get a 201 Created Response Code
            .andExpect(status().isCreated())

            // Validate Headers
            .andExpect(content().contentType(
                                      MediaType.APPLICATION_JSON))
            .andExpect(header().string(HttpHeaders.LOCATION, 
                                       "/widget/1"))
            .andExpect(header().string(HttpHeaders.ETAG, "\"1\""))

            // Validate content
            .andExpect(jsonPath("$.id").value(1L))
            .andExpect(jsonPath("$.name").value("Widget 1"))
            .andExpect(jsonPath("$.version").value(1));

The testCreateWidget() method first creates a Widget to return when the WidgetService’s create() method is called with any argument. The any() matcher matches any argument and, because the createWidget() handler will create a new Widget instance, we will not have access to that instance when the test runs. We then invoke MockMvc’s perform() method to the ”/widgets” URI, sending the content body of a new widget named “Widget 1”, using the content() method. We expect a 201 Created HTTP response code, an “application/json” content type, a location header of “/widget/1”, and an eTag value of the String1”. The body of the response should match the Widget we returned from the create() method, namely an ID of 1, a name of “Widget 1”, and a version of 1.

Unit testing PUT /widget

This code runs three tests for the PUT operation:

@Test
public void testSuccessfulUpdate() throws Exception {
    // Create a mock Widget when the WidgetService's findById(1L) 
    // is called
    Widget mockWidget = new Widget(1L, "Widget 1", 5);
    when(widgetService.findById(1L))
                      .thenReturn(Optional.of(mockWidget));

    // Create a mock Coffee that is returned when the 
    // CoffeeController saves the Coffee to the database
    Widget savedWidget = new Widget(1L, "Updated Widget 1", 6);
    when(widgetService.save(any())).thenReturn(savedWidget);

    // Execute a PUT /widget/1 with a matching version: 5
    mockMvc.perform(put("/widget/{id}", 1L)
                    .contentType(MediaType.APPLICATION_JSON)
                    .header(HttpHeaders.IF_MATCH, 5)
                    .content("{\"id\": 1, " +
                             "\"name\": \"Updated Widget 1\"}"))

            // Validate that we get a 200 OK HTTP Response
           .andExpect(status().isOk())

            // Validate the headers
           .andExpect(content()
                        .contentType(MediaType.APPLICATION_JSON))
           .andExpect(header().string(HttpHeaders.LOCATION, 
                                      "/widget/1"))
           .andExpect(header().string(HttpHeaders.ETAG, "\"6\""))

           // Validate the contents of the response
           .andExpect(jsonPath("$.id").value(1L))
           .andExpect(jsonPath("$.name")
                               .value("Updated Widget 1"))
           .andExpect(jsonPath("$.version").value(6));
}

@Test
public void testUpdateConflict() throws Exception {
   // Create a mock coffee with a version set to 5
   Widget mockWidget = new Widget(1L, "Widget 1", 5);

    // Return the mock Coffee when the CoffeeService's 
    // findById(1L) is called
    when(widgetService.findById(1L))
                      .thenReturn(Optional.of(mockWidget));

    // Execute a PUT /widget/1 with a mismatched version number: 2
    mockMvc.perform(put("/widget/{id}", 1L)
                    .contentType(MediaType.APPLICATION_JSON)
                    .header(HttpHeaders.IF_MATCH, 2)
                    .content("{\"id\": 1, " + 
                             "\"name\":  \"Updated Widget 1\"}"))
             // Validate that we get a 409 Conflict HTTP Response
            .andExpect(status().isConflict());
}

@Test
public void testUpdateNotFound() throws Exception {
   // Return the mock Coffee when the CoffeeService's 
   // findById(1L) is called
   when(widgetService.findById(1L)).thenReturn(Optional.empty());

   // Execute a PUT /coffee/1 with a mismatched version number: 2
   mockMvc.perform(put("/widget/{id}", 1L)
                    .contentType(MediaType.APPLICATION_JSON)
                    .header(HttpHeaders.IF_MATCH, 2)
                    .content("{\"id\": 1, " + 
                             "\"name\":  \"Updated Coffee 1\"}"))

           // Validate that we get 404 Not Found
           .andExpect(status().isNotFound());
}

We have three variations:

  • A successful update.
  • A failed update because of a version conflict.
  • A failed update because the widget was not found.

In RESTful web services, version management is handled by the entity tag, or eTag. When you retrieve an entity, it has an eTag value. When you want to update the entity, you pass that eTag value in the If-Match HTTP header. If the If-Match header does not match the current eTag, which is the Widget version in our implementation, then the PUT handler returns a 409 Conflict HTTP response code. If you get this error, it means that you need to retrieve the entity again and retry your operation. This way, if two different clients attempt to update the same entity simultaneously, only one will succeed.

In the testSuccessfulUpdate() method, we return a Widget with a version of 5 when the WidgetService’s findById() method is called. We then pass an If-Match header value of 5 and then validate that we get a 200 OK HTTP response code and the expected header and body values. In the testUpdateConflict() method, we do the same thing, but we set the If-Match header to 2, which does not match 5, so we validate that we get a 409 Conflict HTTP response code. And finally, in the testUpdateNotFound() method, we configure the WidgetService to return an Optional.empty() when its findById() method is called, so we execute the PUT operation and validate that we get a 404 Not Found HTTP response code.

Unit testing DELETE /widget

Finally, here is the source code for our two DELETE /widget tests:

@Test
void testDeleteSuccess() throws Exception {
    // Setup mocked product
    Widget mockWidget = new Widget(1L, "Widget 1", 5);

    // Setup the mocked service
    when(widgetService.findById(1L))
                      .thenReturn(Optional.of(mockWidget));
    doNothing().when(widgetService).deleteById(1L);

    // Execute our DELETE request
    mockMvc.perform(delete("/widget/{id}", 1L))
            .andExpect(status().isOk());
}

@Test
void testDeleteNotFound() throws Exception {
    // Setup the mocked service
    when(widgetService.findById(1L)).thenReturn(Optional.empty());

    // Execute our DELETE request
    mockMvc.perform(delete("/widget/{id}", 1L))
            .andExpect(status().isNotFound());
}

The DELETE handler first tries to find the widget by ID and then calls the WidgetService’s deleteById() method. The testDeleteSuccess() method configures the WidgetService to return a mock Widget when the findById() method is called and then configures it to do nothing when the deleteById() method is called. The deleteById() method returns void, so we do not need to mock a response, though we do want to allow the method to be called. We execute the DELETE operation and validate that we receive a 200 OK HTTP response code. The testDeleteNotFound() method configures the WidgetService to return Optional.empty() when its findById() method is called. We execute the DELETE operation and validate that we receive a 404 Not Found HTTP response code.

At this point, we have a comprehensive set of tests for all of our controller operations. Let’s continue down our stack and test our service.

Unit testing a Spring MVC service

Next, we’ll test a WidgetService class, shown here:

package com.infoworld.widgetservice.service;

import java.util.List;
import java.util.Optional;

import com.infoworld.widgetservice.model.Widget;
import com.infoworld.widgetservice.repository.WidgetRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class WidgetService {
    @Autowired
    private WidgetRepository widgetRepository;

    public List findAll() {
        return widgetRepository.findAll();
    }

    public Optional findById(Long id) {
        return widgetRepository.findById(id);
    }

    public Widget create(Widget widget) {
        widget.setVersion(1);
        return widgetRepository.save(widget);
    }

    public Widget save(Widget widget) {
        return widgetRepository.save(widget);
    }

    public void deleteById(Long id) {
        widgetRepository.deleteById(id);
    }
}

The WidgetService is very simple. It autowires in a WidgetRepository and then delegates almost all its functionality to the WidgetRepository. The only business logic it implements is that it sets the Widget version to 1 in the create() method, when it is persisting a new Widget to the database.

While Spring supports slice testing for our controller and (as you’ll soon see) our repository, it doesn’t have a slice testing annotation for our service. We could use the @SpringBootTest annotation, but then Spring would load all the controllers, repositories, and any other Spring resources in our application into the Spring application context. We can avoid by using Mockito directly.

Here is the source code for the WidgetServiceTest class:

package com.infoworld.widgetservice.service;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;

import java.util.Optional;

import com.infoworld.widgetservice.model.Widget;
import com.infoworld.widgetservice.repository.WidgetRepository;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class WidgetServiceTest {
    @Mock
    private WidgetRepository repository;

    @InjectMocks
    private WidgetService service;

    @Test
    void testFindById() {
        Widget widget = new Widget(1L, "My Widget", 1);
        when(repository.findById(1L)).thenReturn(Optional.of(widget));

        Optional w = service.findById(1L);
        assertTrue(w.isPresent());
        assertEquals(1L, w.get().getId());
        assertEquals("My Widget", w.get().getName());
        assertEquals(1, w.get().getVersion());
    }
}

JUnit 5 supports extensions and Mockito has defined a test extension that we can access through the @ExtendWith annotation. This extension allows Mockito to read our class, find objects to mock, and inject mocks into other classes. The WidgetServiceTest tells Mockito to create a mock WidgetRepository, by annotating it with the @Mock annotation, and then to inject that mock into the WidgetService, using the @InjectMocks annotation. The result is that we have a WidgetService that we can test and it will have a mock WidgetRepository that we can configure for our test cases.

Also see: Advanced unit testing with JUnit 5, Mockito, and Hamcrest.

This is not a comprehensive test, but it should get you started. It has a single method, testFindById(), that demonstrates how to test a service method. It creates a mock Widget instance and then uses the Mockito when() method, just as we used in the controller test, to configure the WidgetRepository to return an Optional of that Widget when its findById() method is called. Then it invokes the WidgetService’s findById() method and validates that the mock Widget is returned.

Slice testing a Spring Data JPA repository

Next, we’ll slice test our JPA repository (WidgetRepository.java), shown here:

package com.infoworld.widgetservice.repository;

import java.util.List;
import com.infoworld.widgetservice.model.Widget;
import org.springframework.data.jpa.repository.JpaRepository;

public interface WidgetRepository extends JpaRepository {
    List findByName(String name);
}

The WidgetRepository is a Spring Data JPA repository, which means that we define the interface and Spring generates the implementation. It extends the JpaRepository interface, which accepts two arguments:

  • The type of entity that it persists, namely a Widget.
  • The type of primary key, which in this case is a Long.

It generates common CRUD method implementations for us to create, update, delete, and find widgets, and then we can define our own query methods using a specific naming convention. For example, we define a findByName() method that returns a List of Widgets. Because “name” is a field in our Widget entity, Spring will generate a query that finds all widgets with the specified name.

Here is our WidgetRepositoryTest class:

package com.infoworld.widgetservice.repository;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.infoworld.widgetservice.model.Widget;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;

@DataJpaTest
public class WidgetRepositoryTest {
    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private WidgetRepository widgetRepository;

    private final List widgetIds = new ArrayList<>();
    private final List testWidgets = Arrays.asList(
            new Widget("Widget 1", 1),
            new Widget("Widget 2", 1),
            new Widget("Widget 3", 1)
    );

    @BeforeEach
    void setup() {
        testWidgets.forEach(widget -> {
            entityManager.persist(widget);
            widgetIds.add((Long)entityManager.getId(widget));
        });
        entityManager.flush();
    }

    @AfterEach
    void teardown() {
        widgetIds.forEach(id -> {
            Widget widget = entityManager.find(Widget.class, id);
            if (widget != null) {
                entityManager.remove(widget);
            }
        });
        widgetIds.clear();
    }

    @Test
    void testFindAll() {
        List widgetList = widgetRepository.findAll();
        assertEquals(3, widgetList.size());
    }

    @Test
    void testFindById() {
        Widget widget = widgetRepository.findById(
                               widgetIds.getFirst()).orElse(null);

        assertNotNull(widget);
        assertEquals(widgetIds.getFirst(), widget.getId());
        assertEquals("Widget 1", widget.getName());
        assertEquals(1, widget.getVersion());
    }

    @Test
    void testFindByIdNotFound() {
        Widget widget = widgetRepository.findById(
            widgetIds.getFirst() + testWidgets.size()).orElse(null);
        assertNull(widget);
    }

    @Test
    void testCreateWidget() {
        Widget widget = new Widget("New Widget", 1);
        Widget insertedWidget = widgetRepository.save(widget);

        assertNotNull(insertedWidget);
        assertEquals("New Widget", insertedWidget.getName());
        assertEquals(1, insertedWidget.getVersion());
        widgetIds.add(insertedWidget.getId());
    }

    @Test
    void testFindByName() {
        List found = widgetRepository.findByName("Widget 2");
        assertEquals(1, found.size(), "Expected to find 1 Widget");

        Widget widget = found.getFirst();
        assertEquals("Widget 2", widget.getName());
        assertEquals(1, widget.getVersion());
    }
}

The WidgetRepositoryTest class is annotated with the @DataJpaTest annotation, which is a slice-testing annotation that loads repositories and entities into the Spring application context and creates a TestEntityManager that we can autowire into our test class. The TestEntityManager allows us to perform database operations outside of our repository so that we can set up and tear down our test scenarios.

In the WidgetRepositoryTest class, we autowire in both our WidgetRepository and TestEntityManager. Then, we define a setup() method that is annotated with JUnit’s @BeforeEach annotation, so it will be executed before each test case runs. Next, we define a teardown() method that is annotated with JUnit’s @AfterEach annotation, so it will be executed after each test completes. The class defines a testWidgets list that contains three test widgets and then the setup() method inserts those into the database using the TestEntityManager’s persist() method. After it inserts each widget, it saves the automatically generated ID so that we can reference it in our tests. Finally, after persisting the widgets, it flushes them to the database by calling the TestEntityManager’s flush() method. The teardown() method iterates over all Widget IDs, finds the Widget using the TestEntityManager’s find() method, and, if it is found, removes it from the database. Finally, it clears the widget ID list so that the setup() method can rebuild it for the next test. (Note that the TestEntityManager removes entities directly; it does not have a remove by ID method, so we first have to find each Widget and then remove them one-by-one.)

Even though most of the methods being tested are autogenerated and well tested, I wanted to demonstrate how to write several kinds of tests. The only method that we really need to test is the findByName() method because that is the only custom method we define. For example, if we were to define the method as findByNam() instead of findByName(), then the method would not work, so it is definitely worth testing.

Conclusion

Spring provides robust support for testing each layer of a Spring MVC application. In this article, we reviewed how to test controllers, using MockMvc; services, using the JUnit Mockito extension; and repositories, using the Spring TestEntityManager. We also reviewed slice testing as a strategy to reduce testing resource utilization and minimize the time required to execute tests. Slice testing is implemented in Spring using the @WebMvcTest and @DataJpaTest annotations. I hope these examples have given you everything you need to feel comfortable writing robust tests for your Spring MVC applications.

Original Link:https://www.infoworld.com/article/3993538/how-to-test-your-java-applications-with-junit-5.html
Originally Posted: Thu, 30 Oct 2025 09:00:00 +0000

0 People voted this article. 0 Upvotes - 0 Downvotes.

Artifice Prime

Atifice Prime is an AI enthusiast with over 25 years of experience as a Linux Sys Admin. They have an interest in Artificial Intelligence, its use as a tool to further humankind, as well as its impact on society.

svg
svg

What do you think?

It is nice to know your opinion. Leave a comment.

Leave a reply

Loading
svg To Top
  • 1

    Unit testing Spring MVC applications with JUnit 5

Quick Navigation