Unit testing with JUnit 5

JUnit 5 tutorial, part 2: Unit testing Spring MVC with JUnit 5

Unit test a Spring MVC service, controller, and repository with JUnit 5, Mockito, MockMvc, and DBUnit

1 2 3 4 Page 2
Page 2 of 4

The next test method, testFindByIdNotFound(), does the same thing, but it configures the mock WidgetRepository to return an Optional.empty() when its findById() method is called with an argument of 1. We invoke the findById() method and validate that the returned Optional<Widget> is not present.

The testFindAll() method creates two sample Widgets and configures the mock WidgetRepository to return a list containing those two Widgets when its findAll() method is called. It invokes the WidgetService::findAll method and validates that the resulting list contains two elements.

Finally, the testSave() method exercises the WidgetService's business logic to verify that saving a Widget increments its version number. This test is a little different from the others, however. We create a Widget to return when the WidgetRepository's save() method is called, but we specify that we want to return that Widget when the save() method is called with any argument. Our reason is that the WidgetServiceImpl will increment the Widget's version number, so the instance will not be the same as the one we're returning. Note how we've used Mockito's any() method, which is defined in the org.mockito.ArgumentMatchers class. The ArgumentMatchers class provides a host of methods that can match any value of a particular type, any non-null value, specific numeric or boolean values, or even Strings, using a regular expression.

We invoke the WidgetServiceImpl's save() method, passing it our sample widget, then we validate that the return value is not null and that the version number has been incremented.

As you've seen, testing a Spring service is straightforward: We simply mock and configure the behavior of its dependencies, then invoke the service class's methods to validate that the business logic is correct.

Unit testing a Spring web controller

Testing a Spring web controller is a little more difficult than testing a Spring service because we want to be able to test its behavior when handling web requests and returning web responses. In other words, we want to test more than its individual methods. In this section, I'll show you how to use JUnit 5, Mockito, and a new tool—MockMvc— to test the following for our Spring web controller:

  • That a RESTful invocation is routed to the correct controller method.
  • That passed-in values are properly deserialized.
  • That the correct web response is generated, including the HTTP response code, headers, and body values.

Let's begin by looking at the code for the WidgetRestController, shown in Listing 4.

Listing 4. The Spring web controller to be tested (WidgetRestController.java)


package com.geekcap.javaworld.spring5mvcexample.web;

import com.geekcap.javaworld.spring5mvcexample.cache.WidgetCache;
import com.geekcap.javaworld.spring5mvcexample.model.Widget;
import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository;
import com.geekcap.javaworld.spring5mvcexample.service.WidgetService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Optional;

@RestController
public class WidgetRestController {
    private static final Logger logger = LogManager.getLogger(WidgetRestController.class);

    @Autowired
    private WidgetService widgetService;

    @GetMapping("/rest/widget/{id}")
    public ResponseEntity<?> getWidget(@PathVariable Long id) {
        return widgetService.findById(id)
                .map(widget -> {
                    try {
                        return ResponseEntity
                                .ok()
                                .eTag(Integer.toString(widget.getVersion()))
                                .location(new URI("/rest/widget/" + widget.getId()))
                                .body(widget);
                    } catch (URISyntaxException e) {
                        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
                    }
                })
                .orElse(ResponseEntity.notFound().build());
    }

    @GetMapping("/rest/widgets")
    public ResponseEntity<List<Widget>> getWidgets() {
        try {
            return ResponseEntity.ok()
                    .location((new URI("/rest/widgets")))
                    .body(widgetService.findAll());
        } catch (URISyntaxException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    @PostMapping("/rest/widget")
    public ResponseEntity<Widget> createWidget(@RequestBody Widget widget) {
        logger.info("Received widget: name: " + widget.getName() + ", description: " + widget.getDescription());
        Widget newWidget = widgetService.save(widget);

        try {
            return ResponseEntity
                    .created(new URI("/rest/widget/" + newWidget.getId()))
                    .eTag(Integer.toString(newWidget.getVersion()))
                    .body(newWidget);
        } catch (URISyntaxException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    @PutMapping("/rest/widget/{id}")
    public ResponseEntity<Widget> updateWidget(@RequestBody Widget widget, @PathVariable Long id, @RequestHeader(HttpHeaders.IF_MATCH) String ifMatch) {
        // Get the widget with the specified id
        Optional<Widget> existingWidget = widgetService.findById(id);
        if (!existingWidget.isPresent()) {
            return ResponseEntity.notFound().build();
        }

        // Validate that the if-match header matches the widget's version
        if (!ifMatch.equalsIgnoreCase(Integer.toString(existingWidget.get().getVersion()))) {
            return ResponseEntity.status(HttpStatus.CONFLICT).build();
        }

        // Update the widget
        widget.setId(id);
        widget = widgetService.save(widget);

        try {
            // Return a 200 response with the updated widget
            return ResponseEntity
                    .ok()
                    .eTag(Integer.toString(widget.getVersion()))
                    .location(new URI("/rest/widget/" + widget.getId()))
                    .body(widget);
        } catch (URISyntaxException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    @PutMapping("/rest/proper/widget/{id}")
    public ResponseEntity<Widget> updateWidgetProper(@RequestBody Widget widget, @PathVariable Long id, @RequestHeader("If-Match") Integer ifMatch) {
        Optional<Widget> existingWidget = widgetService.findById(id);
        if (existingWidget.isPresent()) {
            if (ifMatch.equals(existingWidget.get().getVersion())) {
                widget.setId(id);
                return ResponseEntity.ok().body(widgetService.save(widget));
            } else {
                return ResponseEntity.status(HttpStatus.CONFLICT).build();
            }
        } else {
            return ResponseEntity.notFound().build();
        }
    }

    @DeleteMapping("/rest/widget/{id}")
    public ResponseEntity deleteWidget(@PathVariable Long id) {
        widgetService.deleteById(id);
        return ResponseEntity.ok().build();
    }
}

About the Spring web controller example class

The WidgetRestController is annotated with the @RestController annotation, which is a combination of the standard @Controller annotation with the @ResponseBody annotation. Controllers are responsible for building a response object and passing it to a view that is presented back to the caller. @RestControllers are responsible for building a data response that is returned as JSON or XML. Therefore, we would typically use the @RestController annotation to implement a RESTful service.

The controller itself handles the plumbing to accept a web request and invoke services that perform the actual business logic to satisfy that request. The WidgetRestController has a WidgetService wired in, which it uses for handling web requests. The controller defines a set of handler methods, annotated with various request mappers: @GetMapping, @PostMapping, @PutMapping, and @DeleteMapping, which correspond to their related HTTP verbs. Each method returns a ResponseEntity that contains the HTTP response code, headers, and optionally a body. For more information about each of these methods, please refer to my tutorial, Mastering Spring framework 5, Part 1: Spring MVC.

The Spring web controller test class

Listing 5 shows the source code for the WidgetRestControllerTest class.

1 2 3 4 Page 2
Page 2 of 4