@ModelAttribute controller spring-mvc mocking

25.5k views Asked by At

I want to test a controller which is using @ModelAttribute for one of its method arguments.

public String processSaveAction(@ModelAttribute("exampleEntity") ExampleEntity exampleEntity)

@ModelAttribute method getExampleEntity is using @RequestParam:

@ModelAttribute("exampleEntity")
public ExampleEntity getExampleEntity(@RequestParam(value = "id", required = true) ExampleEntity exampleEntity) {

My controller is using WebDataBinder to call a factory, which returns an object based on param "id".

@Controller
public class ExampleController(){

    @Autowired private IdEditorFactory idEditorFactory;

    @InitBinder
    public void initBinder(WebDataBinder binder) {

        binder.registerCustomEditor(ExampleEntity.class, idEditorFactory.createEditor(ExampleEntity.class));
    }

    @ModelAttribute("exampleEntity")
    public ExampleEntity getExampleEntity(@RequestParam(value = "id", required = true) ExampleEntity exampleEntity) {

        //Irrelevant operations
        return exampleEntity;
    }

    @RequestMapping(method = RequestMethod.POST, params = "action=save")
    public String processSaveAction(
            @RequestParam(value = "confirmed") String exampleString,
            @ModelAttribute("exampleEntity") ExampleEntity exampleEntity,
            BindingResult result, HttpServletRequest request)
            throws IOException {

        boolean success = editorProcessor.processSaveAction(exampleString,
                exampleEntity, result, request);

        return success ? getSuccessView(exampleEntity) : VIEW_NAME;
    }
}

And my test:

@WebAppConfiguration
public class ExampleControllerTest{

    @Mock private EditorProcessor editorProcessor;
    @Mock private IdEditorFactory idEditorFactory;
    @InjectMocks private ExampleController exampleController;

    private MockMvc mockMvc;


    @Before
    public void setUp() throws Exception {

        MockitoAnnotations.initMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(exampleController).build();

        WebDataBinder webDataBinder = new WebDataBinder(ExampleEntity.class);
        webDataBinder.registerCustomEditor(ExampleEntity.class, idEditorFactory.createEditor(ExampleEntity.class));
    }

    @Test
    public void shouldProcessSaveAction() throws Exception {

        // given
        BindingResult result = mock(BindingResult.class);
        ExampleEntity exampleEntity = mock(ExampleEntity.class);
        HttpServletRequest httpServletRequest = mock(HttpServletRequest.class);

        given(editorProcessor.processSaveAction("confirmed", exampleEntity, result, httpServletRequest)).willReturn(true);

        // when
        ResultActions perform = mockMvc.perform(post("/").sessionAttr("exampleEntity", exampleEntity)
                                                            .param("id", "123456"
                                                            .param("action","save"));

        // then
        perform.andDo(print())
                .andExpect(status().isOk());

    }
}

I want to somehow mock getExampleEntity() so that every time I perform a POST with parameter "id", I receive a mocked object ("exampleEntity") for the test.

I could introduce @Binding to the test, but then I would have to mock many levels of methods (like initBinder -> idEditoryFactory-> editor -> hibernateTemplate and so on) only to get an entity from some source (for example, a database).

3

There are 3 answers

1
Dan P On

You can pass in the required @ModelAttribute object with the .flashAttr() method like so:

mockMvc.perform(post("/")                                                           
    .param("id", "123456")
    .param("action","save")
    .flashAttr("exampleEntity", new ExampleEntity()));
0
Jianchao  Lee On

First, test code shouldn't change our development code. @ModelAttribute will be mount from your param attribute, so .param() is enough. Below is my demo:

    @Test
    public void registerUser() throws Exception {
        System.out.println("hello......." + rob.toString());
        RequestBuilder request = post("/register.html")
            .param("username", rob.getUsername())
            .param("password", rob.getPassword())
            .param("firstName", rob.getFirstName())
            .param("lastName", rob.getLastName())
            .param("email", rob.getEmail())
            .with(csrf());

        mvc
            .perform(request)
            .andDo(MockMvcResultHandlers.print())
            .andExpect(redirectedUrl("/"));
    }

Then is my @Controller:

@Controller
public class LoginController {
    @Autowired
    private UserService userService;

    @RequestMapping(value = "/remove", method = RequestMethod.GET)
    public String removeById(@RequestParam("userid") int id, RedirectAttributes attr) {
        attr.addFlashAttribute("message", "remove!!!");
        attr.addAttribute("mess", "remove ");
        return "redirect:/userlist.html";
    }

    @RequestMapping(value = "/register", method = RequestMethod.POST)
    public String register(@ModelAttribute("user") User user, ModelMap model) {
        System.out.println("register " + user.toString());
        boolean result = userService.add(user);
        model.addAttribute("message", "add " + (result ? "successed" : "failed") + "!!!");
        return "/";
    }
}

This can submit the right user object to the public String register(@ModelAttribute("user") User user, ModelMap model).

0
DaAztechStudent On

I'm new to Spring MVC, and currently writing a @Controller class but none of the methods have business logic, let alone HTML files for the views under'/static/'. First, I wanted to see how I can Unit Test every method to make sure all end points responded 200/ok before I inserted the business logic, you know Test Driven Development. Then I had difficulty when unit testing @PostMapping annotation method that had a @ModelAttribute assigned to it. After my whole search yesterday, I put together code for someone to unit test such cases involving @PostMapping and @ModelAttribute where you need to update the parameter values of your model attribute on the 'post' method. I'm more than welcome positive feedback to make my tests better, just wanted to post this in cases someone else that's also new wanted to test and make sure that the new info will be saved after the post in the @ModelAttribute without needing a html/jsp file for views for standalone unit testing, look at @Controller2 and String updateQuoteRequest() method for reference, and the last test in class QuoteRequestManagementController_UnitTests for more details.

pom.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-mvc-hotel-app</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>serving-web-content</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!--  Spring Boot Test Starter is Starter for testing Spring Boot applications 
with libraries including JUnit, Hamcrest and Mockito. -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Model Attribute Class:

package com.corplithotel.eventsapp.domain;

//Create the Model Attribute class, and its class members

public class QuoteRequest {

    String customer;

    String age;

    String budget;

    String eventType;

    String foodAllergies;

    //getters and setters

    public String getCustomer() {
        return customer;
    }

    public void setCustomer(String customer) {
        this.customer = customer;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    public String getBudget() {
        return budget;
    }

    public void setBudget(String budget) {
        this.budget = budget;
    }

    public String getEventType() {
        return eventType;
    }

    public void setEventType(String eventType) {
        this.eventType = eventType;
    }

    public String getFoodAllergies() {
        return foodAllergies;
    }

    public void setFoodAllergies(String foodAllergies) {
        this.foodAllergies = foodAllergies;
    }
}

Main Class:

package com.corplithotel.eventsapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


@SpringBootApplication
public class CorpLitHotel {


    public static void main(String[] args) {
        SpringApplication.run(CorpLitHotel.class, args);
    }
}

@Controller1

package com.corplithotel.eventsapp.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import com.corplithotel.eventsapp.domain.QuoteRequest;

//Step 1: * Create QuoteRequestController *
/*@Conroller annotation makes this class a controller, next we need to
*  add 'handler mappings' to provide the controller some functionality.
*  For Step 1, we won't add logic for @RequestMapping 'beginQuoteRequest()'
*  & @Postrequest 'submitQuoteRequest()' methods, we will Mock the class 
*  and unit test in Step 2 for TDD examples:
*  
* 
*/
@Controller
public class QuoteRequestController {

    /*@GetMapping annotation is a 'handler mapping' annotation.
     * When a user comes to the page to fill out the Quote form, they 
     * first need to get the page. The return of the method will be a
     * 'logical view name', which is just a string, and tends to correlate
     * to some HTML, JSP or whatever file you're using for your View. 
     * 
     */
    @GetMapping("/newquote")
    public String beginQuoteRequest(Model model) {
        //Check Unit Test for logic
    
        return "newQuote";
    }//beginQuoteRequest()

    /*@PosMapping annotation is another 'handler mapping' annotation.
     * Once a user fills out the Quote form with their name and 
     * other event details, they may want to save or post that quote.
     * We need to add a handler for the Post, and needs to be a separate
     * method. Will be a separate page with a confirmation message to let 
     * the user know their Quote request has been received.
     */
    @PostMapping("/newquote")
    public String submitQuoteRequest(@ModelAttribute QuoteRequest formBean) {
        //Check Unit Test for ideal logic
        return "newQuoteConfirmation";
    }//submitQuoteRequest()
}

Controller 1 Unit Tests:

package com.corplithotel.eventsapp.controller;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders; 
import org.springframework.web.context.WebApplicationContext;

import com.corplithotel.eventsapp.domain.QuoteRequest;


/*Step 2 *Create Unit tests for QuoteRequestController*: 
 * this tests are assuming 
 */
@ExtendWith(MockitoExtension.class)
@WebMvcTest( QuoteRequestController.class)
@TestInstance(Lifecycle.PER_CLASS)


public class QuoteRequestController_UnitTests {

    @Mock
    private WebApplicationContext wac;

    @InjectMocks
    private QuoteRequestController qrc;
    private MockMvc qrcMockMvc;

    @BeforeAll
    public void setUp() {
        qrcMockMvc  = MockMvcBuilders.standaloneSetup(qrc).build();
    }//BeforeAll

    @Test
    @DisplayName("testGetQuoteForm.. beginQuoteRequest().. Expected to pass..")
    public void testGetQuoteForm() throws Exception {
        //simulate getting a new form for the user to fill in (GET)
        qrcMockMvc
            .perform(get("/newquote"))
            .andExpect(status().is(200))
            .andReturn();
    }//testGetQuoteForm()

    @Test 
    @DisplayName("testPostQuoteForm().. submitQuoteRequest.. Expected to pass..")
    public void testPostQuoteForm() throws Exception {
        QuoteRequest aFormBean = new QuoteRequest();
        qrcMockMvc
            .perform(post("/newquote", aFormBean))
            .andExpect(status().isOk())
            .andReturn();
    }//testGetQuoteForm()

}// QuoteRequestController_UnitTests

Result 1:

Junit Controller 1 Results

Controller 2:

package com.corplithotel.eventsapp.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.corplithotel.eventsapp.domain.QuoteRequest;

/*Step 3 *Creating QuoteRequestManagementController
 * This is the controller that the sales team member
 * uses to reply to a customer's request for an estimate. 
 * Sale's Team member can see all the incoming requests.
 *
 *Controller method's body for Step 3 will be empty, we will unit test 
 * every method of the Controller first in Step 4.  
 */
@Controller
public class QuoteRequestManagementController {
    /*
     * We will be specifying, parameters, look for a parameter of
     *  a  particular value; or looking for the absence of a parameter 
     */

    //Specifying: Sale's Team member can see all the incoming requests.
    @GetMapping(path = "/quoteRequests")
    public String  listQuoteRequests() {
    
        return "quoteRequestsList";
    
    }//listRequests()


    /*Parameter Of A Specific Value: Narrow down search for different
     * types of sales reps. Look for 'eventType' = 'wedding' for sales reps that
     * only deal with weddings and only see events associated 
     * with weddings.  
     */
    @GetMapping(path = "/quoteRequests", params="eventType=wedding")
    public String  listWeddingRequests() {
    
        return "quoteWeddingRequestsList";
    
    }//listWeddingRequests()

    /*Parameter Of A Specific Value: Narrow down search for different types of sales 
reps.
     * Look for 'eventType' = 'birthday' for sales reps that
     * only deal with weddings and only see events associated 
     * with weddings.  
     */
    @GetMapping(path = "/quoteRequests", params="eventType=birthday")
    public String  listBirthdayRequests() {
    
        return "quoteBirthdayRequestsList";
    
    }//listBirthdayRequests()


    /*
     * Look for 'eventType' parameter regardless of its value
     */
    @GetMapping(path = "/quoteRequests", params="eventType")
    public String  listAllEventTypeRequests() {
    
        return "quoteAllEventTypeRequestList";
    
    }//listAllEventTypeRequests()

    /*
     * Absence of a parameter: Look for requests with no 'eventType' parameter 
     */
    @GetMapping(path = "/quoteRequests", params="!eventType")
    public String  listNoneEventTypeRequests() {
    
        return "quoteNoneEventTypeRequestsList";
    
    }//listNoneEventTypeRequests() 



    /*
     * Specifying: Create another mapping for a sales rep to drill down
     *  from what I see in a list and pick one particular quote
     *  request. We will accomplish this by providing each 
     *  quote request a unique quoteID using @PathVariable
     */

    @GetMapping("/quoteRequests/{quoteID}")
    public String viewQuoteRequest(@PathVariable int quoteID) {
    
        //refer to quoteID in my implementation
        return "quoteRequestsDetails";
    }//viewQuoteRequest()

    /*
     *For this scenario lets say a sales rep is in a particular 
     * quote and maybe want to add a note, which will require them
     * to save the content of the screen. This means we need a
     * @PostMapping. The sales rep might want to update the customer  
     * name, event type, food allergy side note, etc.
     * 
     *Once they hit 'save', all the data will come in and be accessible
     * through @ModelAttribute and we can reference the Model Attribute in 
     * the method signature. So as we implement the logic in the controller
     * we get to use a Model Bean and pull in all the updated data 
     * and ultimately save the data somewhere. 
     */

    @PostMapping ("/quoteUpdateDetails")
    public String updateQuoteRequest(
            @ModelAttribute("quoteRequest") QuoteRequest quoteRequest) {
    
        //implement a save of all the form bean information
    
        return "quoteUpdateDetails";
    
    }//updateQuoteRequest() 
}

Controller 2 Unit Test:

package com.corplithotel.eventsapp.controller;

import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import 
org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

import com.corplithotel.eventsapp.domain.QuoteRequest;


@ExtendWith(MockitoExtension.class)
@WebMvcTest( QuoteRequestManagementController.class)
@TestInstance(Lifecycle.PER_CLASS)
class QuoteRequestManagementController_UnitTests {

    @Mock
    private WebApplicationContext wac;

    @InjectMocks
    private QuoteRequestManagementController qrmc;
    private MockMvc qrmcMockMvc;


    @BeforeAll
    public void setUp() {
    
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/");
        viewResolver.setSuffix(".html");
        
        qrmcMockMvc= 
            MockMvcBuilders.standaloneSetup(qrmc)
            .setViewResolvers(viewResolver).build();
    }//BeforeAll


    @Test
    @DisplayName("testListQuoteRequests().. Test should pass")
    void testlistQuoteRequests() throws Exception {
    
        qrmcMockMvc
            .perform(get("/quoteRequests"))
            .andExpect(status().is(200))
            .andReturn();
    
    }//testlistRequests()

    @Test
    @DisplayName("testListWeddingRequests() .. Parameter Of A Specific Value Test1.. Test should pass")
    void testlistWeddingRequests() throws Exception {
    
        qrmcMockMvc
            .perform(get("/quoteRequests?eventType=wedding"))
            .andExpect(status().is(200))
            .andReturn();
    
    }//testlistWeddingRequests()

    @Test
    @DisplayName("testListBirthdayRequests() .. Parameter Of A Specific Value Test2.. Test should pass")
    void testlistBirthdayRequests() throws Exception {
    
        qrmcMockMvc
            .perform(get("/quoteRequests?eventType=birthday"))
            .andExpect(status().is(200))
            .andReturn();
    
    }//testlistBirthdayRequests()

    @Test
    @DisplayName("testListAllEventsRequests() .. Parameter with no specified value.. Test should pass")
    void testlistAllEventsRequests() throws Exception {
    
        qrmcMockMvc
            .perform(get("/quoteRequests?eventType"))
            .andExpect(status().is(200))
            .andReturn();
    
    }//testlistBirthdayRequests()

    @Test
    @DisplayName("testNoneEventTypeRequests() .. no parameter .. Test should pass")
    void testNoneEventTypeRequests() throws Exception {
    
        qrmcMockMvc
            .perform(get("/quoteRequests?!eventType"))
            .andExpect(status().is(200))
            .andReturn();
    
    }//testlistBirthdayRequests()

    @Test
    @DisplayName("testViewQuoteRequest().. by 'quoteID'.. Test should pass")
    void testViewQuoteRequest() throws Exception {
        qrmcMockMvc
            .perform(get("/quoteRequests/{quoteID}", 4))
            .andExpect(status().is(200))
            .andReturn();
    }//testViewQuoteRequest()

    @Test
    @DisplayName("test2ViewQuoteRequest().. by 'quoteID'.. Test should pass")
    void tes2tViewQuoteRequest() throws Exception {
        qrmcMockMvc
            .perform(get("/quoteRequests/{quoteID}", 415))
            .andExpect(status().is(200))
            .andReturn();
    }//testViewQuoteRequest()

    @Test
    void testupdateQuoteRequest() throws Exception {
    
        MockHttpServletRequestBuilder updateDetails = post("/quoteUpdateDetails")
                .param("customer", "Joe")
                .param("age", "12")
                .param("budget", "$1209")
                .param("eventType", "wedding")
                .param("foodAllergies", "fish")
                .flashAttr("quoteRequest", new QuoteRequest());
            
    
         qrmcMockMvc
            .perform( updateDetails)
            .andExpect(status().is(200));
     

    }
}