@ModelAttribute控制器Spring-MVC模拟测试

11

我想测试一个控制器,它在其中一个方法参数中使用了@ModelAttribute

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

@ModelAttribute 方法 getExampleEntity 使用了 @RequestParam

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

我的控制器使用 WebDataBinder 来调用一个工厂,该工厂根据参数 "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;
    }
}

我的测试:

@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());

    }
}

我希望能够模拟 getExampleEntity() 方法,使得每次通过“id”参数进行POST时,都能够收到一个名为“exampleEntity”的模拟对象,以用于测试。

我可以在测试中使用@Binding,但这样我就必须对许多方法进行模拟(比如initBinder-> idEditorFactory-> editor-> hibernateTemplate等),才能从某个来源(例如数据库)获取一个实体。

3个回答

28

您可以使用.flashAttr()方法,将所需的@ModelAttribute对象传递进去:

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

1
感谢您的回答。我也遇到了同样的问题,使用 .flashAttr() 方法解决了它。 - YLombardi

7

首先,测试代码不应该改变我们的开发代码。@ModelAttribute将从您的参数属性中挂载,因此.param()就足够了。以下是我的演示:

    @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("/"));
    }

然后是我的@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 "/";
    }
}

这可以将正确的用户对象提交给 public String register(@ModelAttribute("user") User user, ModelMap model)


0
我是Spring MVC的新手,目前正在编写一个@Controller类,但其中没有任何方法具有业务逻辑,更不用说在'/static/'下的视图HTML文件了。首先,我想看看如何对每个方法进行单元测试,以确保在插入业务逻辑之前所有端点都响应200/ok,你知道的,测试驱动开发。然后,当我对一个带有@ModelAttribute分配的@PostMapping注释方法进行单元测试时遇到了困难。昨天经过我的整个搜索,我为某人组合了代码,以单元测试涉及@PostMapping和@ModelAttribute的这种情况,在其中您需要在'post'方法上更新模型属性的参数值。我非常欢迎积极的反馈,以使我的测试更好,只是想在这种情况下发布此内容,以便其他新手也可以测试并确保在@ModelAttribute中的post之后保存新信息,而无需为独立单元测试查看html/jsp文件视图,请参考@Controller2和String updateQuoteRequest()方法,并查看类QuoteRequestManagementController_UnitTests中的最后一个测试以获取更多详细信息。

pom.xml文件:

<?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>

模型属性类:

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;
    }
}

主类:

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);
    }
}

@控制器1

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()
}

控制器1单元测试:

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

结果 1:

Junit 控制器 1 结果

控制器 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() 
}

控制器 2 单元测试:

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));
     

    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接