如何将 @WebMvcTest 用于单元测试 POST 方法?

Posted

技术标签:

【中文标题】如何将 @WebMvcTest 用于单元测试 POST 方法?【英文标题】:How can I use @WebMvcTest for Unit Test POST method? 【发布时间】:2019-03-12 09:13:56 【问题描述】:

我正在使用 Spring Boot 和 Mockito 运行单元测试,并且正在测试 RESTful 服务。当我尝试测试 GET 方法时,它可以成功运行,但是当我尝试测试 POST 方法时,它会失败。我应该怎么做才能解决这个问题?提前致谢!

这是 REST 控制器的代码:

package com.dgs.restfultesting.controller;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import com.dgs.restfultesting.business.ItemBusinessService;
import com.dgs.restfultesting.model.Item;

@RestController
public class ItemController 

    @Autowired
    private ItemBusinessService businessService;

    @GetMapping("/all-items-from-database")
    public List<Item> retrieveAllItems() 
        return businessService.retrieveAllItems(); 
    

    @PostMapping("/items")
    public Item addItem(@RequestBody Item item) 
        Item savedItem = businessService.addAnItem(item); 

        return savedItem;
    

业务层:

package com.dgs.restfultesting.business;

import java.util.List;

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

import com.dgs.restfultesting.data.ItemRepository;
import com.dgs.restfultesting.model.Item;

@Component
public class ItemBusinessService 

    @Autowired
    private ItemRepository repository;

    public Item retrieveHardcodedItem() 
        return new Item(1, "Book", 10, 100); 
    

    public List<Item> retrieveAllItems() 

        List<Item> items = repository.findAll(); 

        for (Item item : items) 
            item.setValue(item.getPrice() * item.getQuantity());  
        

        return items;  
    

    public Item addAnItem(Item item) 
        return repository.save(item); 
    

ItemControllerTest:

package com.dgs.restfultesting.controller;

import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.Arrays;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import com.dgs.restfultesting.business.ItemBusinessService;
import com.dgs.restfultesting.model.Item;

@RunWith(SpringRunner.class)             
@WebMvcTest(ItemController.class)  
public class ItemControllerTest 

    @Autowired
    private MockMvc mockMvc;    

    @MockBean
    private ItemBusinessService businessService;

    @Test
    public void retrieveAllItems_basic() throws Exception 

        when(businessService.retrieveAllItems()).thenReturn(
                Arrays.asList(new Item(2, "iPhone", 1000, 10),
                        new Item(3, "Huawei", 500, 17)));

        RequestBuilder request = MockMvcRequestBuilders
                .get("/all-items-from-database") 
                .accept(MediaType.APPLICATION_JSON); 

        MvcResult result = mockMvc.perform(request)
                .andExpect(status().isOk())
                .andExpect(content().json("[id:2, name:iPhone, price:1000, id:3, name:Huawei, price:500]"))  // This will return an array back, so this data should be within an array
                .andReturn();  
       

    @Test
    public void createItem() throws Exception 
        RequestBuilder request = MockMvcRequestBuilders
                .post("/items")
                .accept(MediaType.APPLICATION_JSON)
                .content("\"id\":1,\"name\":\"Book\",\"price\":10,\"quantity\":100")
                .contentType(MediaType.APPLICATION_JSON);

        MvcResult result = mockMvc.perform(request)
                .andExpect(status().isCreated())
                .andExpect(header().string("location", containsString("/item/")))
                .andReturn();
    

测试retrieveAllItems_basic() 方法没有问题,但是当我尝试为createItem() 方法运行JUnit 测试时,它不起作用,我得到这个:java.lang.AssertionError: Status expected: 但是:

这是我在控制台中收到的:

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /items
       Parameters = 
          Headers = Content-Type=[application/json], Accept=[application/json]
             Body = <no character encoding set>
    Session Attrs = 

Handler:
             Type = com.dgs.restfultesting.controller.ItemController
           Method = public com.dgs.restfultesting.model.Item com.dgs.restfultesting.controller.ItemController.addItem(com.dgs.restfultesting.model.Item)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = 
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
2018-10-07 17:53:51.457  INFO 55300 --- [       Thread-3] o.s.w.c.s.GenericWebApplicationContext   : Closing org.springframework.web.context.support.GenericWebApplicationContext@71075444: startup date [Sun Oct 07 17:53:48 EEST 2018]; root of context hierarchy

更新 -----------------

我尝试像这样设置位置:item/id。

这是控制器的代码:

@PostMapping("/items")
public ResponseEntity<Object> addItem(@RequestBody Item item) 
    Item savedItem = businessService.addAnItem(item); 
    HttpHeaders httpHeaders = new HttpHeaders();
    UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance();

    UriComponents uriComponents =
            uriComponentsBuilder.path("/item/id").buildAndExpand(savedItem.getId());
    httpHeaders.setLocation(uriComponents.toUri());

    return new ResponseEntity<>(savedItem, httpHeaders, HttpStatus.CREATED); 

这是测试代码:

@Test
public void createItem() throws Exception 

    RequestBuilder request = MockMvcRequestBuilders
            .post("/items")
            .accept(MediaType.APPLICATION_JSON)
            .content("\"id\":1,\"name\":\"Book\",\"price\":10,\"quantity\":100")
            .contentType(MediaType.APPLICATION_JSON);

    MvcResult result = mockMvc.perform(request)
            .andExpect(status().isCreated())
            .andExpect(header().string("location", containsString("/item/1")))
            .andReturn();

当我为 createItem() 方法运行 JUnit 测试时,我收到此错误:org.springframework.web.util.NestedServletException: Request processing failed;嵌套异常是 java.lang.NullPointerException

【问题讨论】:

【参考方案1】:

从您的控制器返回 201: 因为您的断言测试通过使用 created 状态期待 201,但您的控制器返回 200(OK)。

   @PostMapping("/items")
    public ResponseEntity<?> addItem(@RequestBody Item item) 
        Item savedItem = itemBusinessService.addAnItem(item);

        return new ResponseEntity<>(savedItem, HttpStatus.CREATED);
    

或修改您的测试以检查状态 OK(200)。 如果您不想断言“位置”,请更新您的测试。

 @Test
 public void createItem() throws Exception 
 RequestBuilder request = MockMvcRequestBuilders
        .post("/items")
        .accept(MediaType.APPLICATION_JSON)
        .content("\"id\":1,\"name\":\"Book\",\"price\":10,\"quantity\":100")
        .contentType(MediaType.APPLICATION_JSON);

MvcResult result = mockMvc.perform(request)
        .andExpect(status().isOk()).andReturn();

更新--允许响应的位置标头

如果您希望 "location" 从标头返回,则修改您的控制器和下面的测试用例以检查标头中的位置。

第 1 步:在控制器的添加项目方法中,添加位置 uri 并返回。

 @PostMapping("/items")
    public ResponseEntity<?> addItem(@RequestBody Item item) 
        Item savedItem = businessService.addAnItem(item);
        HttpHeaders httpHeaders = new HttpHeaders();
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance();

        UriComponents uriComponents =
                uriComponentsBuilder.path("/item/").buildAndExpand("/item/");
        httpHeaders.setLocation(uriComponents.toUri());
        return new ResponseEntity<>(savedItem, httpHeaders, HttpStatus.CREATED);
    

第 2 步:现在您的测试将按照您的预期断言 "location"

 @Test
    public void createItem() throws Exception 
        RequestBuilder request = MockMvcRequestBuilders
                .post("/items")
                .accept(MediaType.APPLICATION_JSON)
                .content("\"id\":1,\"name\":\"Book\",\"price\":10,\"quantity\":100")
                .contentType(MediaType.APPLICATION_JSON);

        MvcResult result = mockMvc.perform(request)
                .andExpect(status().isCreated())
                .andExpect(header().string("location", containsString("/item/")))
                .andReturn();
    

【讨论】:

感谢重播,但它不起作用,我收到此错误:java.lang.AssertionError: Response header 'location' Expected: a string contains "/item/" but: was null @elvis 使用我从答案中更新的测试来获得成功结果,在您的测试用例中,我看到您正在检查位置,但要返回您需要在标题中设置的位置,以便检查项目是创建或不运行我最近发布的测试..对于位置,我将建议您通过一些示例了解如何在响应标头中设置位置,然后您也可以在测试中添加该行。 @elvis 我已经更新了答案,您如何也可以从您的响应中返回位置 uri 以匹配 ..检查我的答案。 谢谢kj007,它现在​​可以工作了。想问你点别的,这样修改代码怎么办:UriComponents uriComponents = uriComponentsBuilder.path("/item/id").buildAndExpand(savedItem.getId()); 这也适用于位置项/id..您只需要在标题中设置位置..确保在您的测试中您与您通过的位置匹配【参考方案2】:

首先我在你的 createItem 测试中没有看到模拟程序部分让我们说

Item item = new Item();
Item newItem = new Item();
when(businessService.addAnItem(item)).thenReturn(newItem);

在您的控制器中,我没有看到 Location 标头。可能像下面这样的代码应该更好:

@PostMapping("/items")
public ResponseEntity<?> addItem(@RequestBody Item item) 
    Item savedItem = itemBusinessService.addAnItem(item);

    return ResponseEntity.created(UriComponentsBuilder.fromHttpUrl("http://yourserver/item"));

希望对你有帮助

【讨论】:

以上是关于如何将 @WebMvcTest 用于单元测试 POST 方法?的主要内容,如果未能解决你的问题,请参考以下文章

单元测试 Spring Boot 应用服务层

如何在@WebMvcTest 测试中忽略@EnableWebSecurity 注释类

如何使用@WebMvcTest 春季测试在模拟服务中注入模拟的restTemplate

@WebMVCTest中的404问题分析

@WebMvcTest 可以有两个控制器吗

使用 @WebMvcTest 测试中的 ApplicationContext 异常