【问题标题】:@ModelAttribute controller spring-mvc mocking@ModelAttribute 控制器 spring-mvc 模拟
【发布时间】:2015-02-13 11:47:14
【问题描述】:

我想测试一个控制器,它使用@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 -> idEditoryFactory-> editor -> hibernateTemplate 等等),才能从某个来源获取实体(例如例如,数据库)。

【问题讨论】:

    标签: unit-testing spring-mvc mocking spring-test-mvc mockmvc


    【解决方案1】:

    您可以使用.flashAttr() 方法传入所需的@ModelAttribute 对象,如下所示:

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

    【讨论】:

    • 感谢您的回答。我遇到了同样的问题, .flashAttr() 方法解决了它。
    【解决方案2】:

    首先,测试代码不应更改我们的开发代码。 @ModelAttribute 将从您的 param 属性挂载,所以 .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)

    【讨论】:

      【解决方案3】:

      我是 Spring MVC 的新手,目前正在编写一个 @Controller 类,但没有一个方法具有业务逻辑,更不用说“/static/”下的视图的 HTML 文件了。首先,我想看看如何对每个方法进行单元测试,以确保在插入业务逻辑之前所有端点都响应 200/ok,你知道测试驱动开发。然后,我在对分配有 @ModelAttribute 的 @PostMapping 注释方法进行单元测试时遇到了困难。在我昨天的整个搜索之后,我为某人整理了代码来对涉及@PostMapping 和@ModelAttribute 的此类案例进行单元测试,您需要在“post”方法上更新模型属性的参数值。我非常欢迎积极的反馈以使我的测试更好,只是想发布这个以防其他新人也想测试并确保新信息将在发布后保存在 @ModelAttribute 中而无需 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);
          }
      }
      

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

      控制器 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 Controller 1 Results

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-11-03
        • 1970-01-01
        • 1970-01-01
        • 2016-11-18
        • 1970-01-01
        • 2016-08-18
        • 1970-01-01
        相关资源
        最近更新 更多