【问题标题】:Spring REST controller behaves differently in unit testsSpring REST 控制器在单元测试中的行为不同
【发布时间】:2020-04-30 15:46:47
【问题描述】:

问题

我是 Spring 新手,正在尝试为我的 REST 控制器编写一些单元测试。使用httpiecurl 手动测试效果很好,但是使用@WebMvcTest 会发生奇怪的事情。

当我 PUTcurl 的新用户时会发生以下情况:

$ curl -v -H'Content-Type: application/json' -d@- localhost:8080/api/users <john_smith.json                                                                                                                                  
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /api/users HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.69.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 102
> 
* upload completely sent off: 102 out of 102 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sat, 18 Apr 2020 22:29:43 GMT
< 
* Connection #0 to host localhost left intact
{"id":1,"firstName":"John","lastName":"Smith","email":"john.smith@example.com","password":"l33tp4ss"}

如您所见,响应中包含 Content-Type 标头,正文确实是新的User

以下是我尝试自动测试的方法:

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private UserService service;

    private final User john = new User("John", "Smith",
                                       "john.smith@example.com",
                                       "s3curep4ss");

    @Test
    public void givenNoUser_whenCreateUser_thenOk()
    throws Exception
    {
        given(service.create(john)).willReturn(john);

        mvc.perform(post("/users")
                    .contentType(APPLICATION_JSON)
                    .content(objectToJsonBytes(john)))
        .andExpect(status().isOk())
        .andExpect(content().contentType(APPLICATION_JSON))
        .andExpect(jsonPath("$.id", is(0)))
        .andDo(document("user"));
    }

}

但我得到的是:

$ mvn test
[...]
MockHttpServletRequest:                                                                                                                
      HTTP Method = POST    
      Request URI = /users                   
       Parameters = {}                                                                                                                 
          Headers = [Content-Type:"application/json", Content-Length:"103"]                                                                                                                                                                                     
             Body = {"id":0,"firstName":"John","lastName":"Smith","email":"john.smith@example.com","password":"s3curep4ss"}
    Session Attrs = {}                                                                                                                                                                                                                                                        

Handler:                
             Type = webshop.controller.UserController
           Method = webshop.controller.UserController#create(Base)                                                                     

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 = []                               
[ERROR] Tests run: 6, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 11.271 s <<< FAILURE! - in webshop.UserControllerTest          
[ERROR] givenNoUser_whenCreateUser_thenOk  Time elapsed: 0.376 s  <<< FAILURE!                                                         
java.lang.AssertionError: Content type not set
        at webshop.UserControllerTest.givenNoUser_whenCreateUser_thenOk(UserControllerTest.java:70)

发生了什么? MockHttpServletResponse 的尸体在哪里?我一定错过了什么,因为它的行为似乎完全不同。


其他代码以备不时之需

我的通用控制器类:

public class GenericController<T extends Base>
implements IGenericController<T> {

    @Autowired
    private IGenericService<T> service;

    @Override
    @PostMapping(consumes = APPLICATION_JSON_VALUE,
                 produces = APPLICATION_JSON_VALUE)
    public T create(@Valid @RequestBody T entity)
    {
        return service.create(entity);
    }

    /* ... Other RequestMethods ... */

}

实际的User控制器:

@RestController
@RequestMapping(path="/users")
public class UserController extends GenericController<User> { }

2020 年 4 月 22 日更新
正如建议的那样,我将泛型排除在外,但这并没有帮助。

【问题讨论】:

  • docs.spring.io/spring/docs/current/javadoc-api/org/…,你不使用springMediaType类有什么原因吗?
  • @ThomasAndolf 啊,谢谢。 _UTF8 版本当时正在重新发明轮子,也是不必要的。我现在完全删除了它。但是,这似乎与我的问题无关(我还用正确的MediaType 更新了我的问题)。
  • uris 显示不同:/api/users vs /users。这有什么关系吗?
  • 我怀疑但我不确定您的模拟是否返回 null。主要是因为你说你想要一个特定的对象,而 java 是按值传递的,所以它想要那个特定的对象而不是任何其他对象。另一方面,您的控制器传递了一个新创建的对象,该对象不是那个特定的值。请检查和调试,看看service.create(entity) 返回什么?
  • 你能打印出模拟返回的内容吗?或调试并设置调试点并查看模拟返回的内容,以便我可以看到您要发送回的内容。

标签: java spring spring-boot spring-mvc


【解决方案1】:

似乎@WebMvcTest 注释正在配置一个使用实际实现的UserService bean,而您的 bean 不知何故被忽略了。

我们可以尝试以不同的方式创建UserService bean

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
@Import(UserControllerTest.Config.class)
public class UserControllerTest {

    @TestConfiguration
    static class Config {

        @Primary
        @Bean
        UserService mockedUserService() {
            UserService service = Mockito.mock(UserService.class);
            given(service.create(john)).willReturn(UserControllerTest.john());
            return service;
        }
    }

    static User john() {
        return new User("John", "Smith", "john.smith@example.com", "s3curep4ss");
    }

    ...
}

您还可以在测试中将存根移至 @Before 方法

@Configuration
public class CommonTestConfig {
   @Primary
   @Bean
   UserService mockedUserService() {
      return Mockito.mock(UserService.class)
   }
}

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
@Import(CommonTestConfig.class)
public class Test1 {
   @Autowired
   private UserService userService;

   @Before
   public void setup() {
      given(userService.create(any())).willReturn(user1());
   }
}

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
@Import(CommonTestConfig.class)
public class Test2 {
   @Autowired
   private UserService userService;

   @Before
   public void setup() {
      given(userService.create(any())).willReturn(user2());
   }
}

【讨论】:

  • 谢谢,但您的回答似乎不起作用。我是否正确理解我应该从我的UserControllerTest 中删除@MockBean private UserService service,而是将这个@TestConfiguration 块放在那里?此外,看起来您在测试用例之前定义了模拟的作用(例如,给出.create(john),返回john())。我不应该能够在每次测试的基础上做到这一点吗?我的意思是在一项测试中,我想尝试没有用户时会发生什么。另一方面,我想假装有一个用户并检查它是否由GET 请求返回。
  • 不,您也可以在 @Before 方法中存根模拟。查看我的更新答案
  • 当我这样做时,我得到Field repository in UserService required a bean of type 'IUserRepository' that could not be found. 我不明白,我只是创建一个看起来和行为都像UserService 但实际上是实际上不是UserService 实例(因此不需要IUserRepository)?
  • 我发现了问题!我看到您使用了.create(any()) 而不是在那里放置User 对象,所以我尝试了这种方式并且它有效。似乎问题出在given(service.create(john)).willReturn(john)这一行。
【解决方案2】:

显然,问题出在这一行:

given(service.create(john)).willReturn(john);

当我将第一个 john(这是一个 User 对象)更改为例如any(),测试通过就好了。


有人可以帮我解释一下这是为什么吗?将johnany() 交换是可行的,但感觉有些笨拙。控制器将 JSON 反序列化的 john 对象传递给它的服务。难道仅仅是反序列化的john 显然和我在测试类中创建的不是同一个对象吗?

【讨论】:

  • 你确定在你的Ueer 类上正确实现了equals 吗?
  • 哦,我没有意识到我必须这样做。你钉了它,现在它完美地工作了。谢谢。
  • 是的,你需要它。 equals() 是您必须实现的方法,以便 Java 测试对象是否“有意义地相等”。没有它,Java 不知道两个不同的对象是否应该被认为是相同的——即使它们的状态是相同的。 (您还应该实现hashcode() 以保持一致性。)
猜你喜欢
  • 1970-01-01
  • 2016-11-14
  • 1970-01-01
  • 2012-05-25
  • 1970-01-01
  • 2020-08-07
  • 2016-01-31
  • 1970-01-01
  • 2018-07-31
相关资源
最近更新 更多