Spring REST контроллер ведет себя по-разному в модульных тестах - PullRequest
3 голосов
/ 18 апреля 2020

Проблема

Я новичок в Spring и пытаюсь написать некоторые модульные тесты для моего контроллера REST. Тестирование вручную с помощью httpie или curl работает хорошо, однако с @WebMvcTest происходят странные вещи.

Вот что происходит, когда я PUT новым пользователем с помощью curl:

$ 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? Я, должно быть, что-то упускаю, так как кажется, что он действует совершенно по-другому.


Другой код, если это необходимо

Мой универсальный c класс контроллера:

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-04-22
Как и предполагалось, я взял дженерики из уравнения , но это не помогло.

Ответы [ 2 ]

3 голосов
/ 22 апреля 2020

Похоже, что аннотация @WebMvcTest настраивает компонент UserService, использующий реальную реализацию, и ваш компонент почему-то игнорируется.

Мы можем попытаться создать компонент UserService по-другому

@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());
   }
}
2 голосов
/ 25 апреля 2020

Очевидно, проблема была в этой строке:

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

Когда я изменяю первый john (который является User объектом), например, any(), тест проходит очень хорошо.


Может ли кто-нибудь пролить свет на то, почему это так? Обмен john с any() работает, но чувствует себя немного хакерским. Контроллер передает десериализованный объект JSON 1012 * своему сервису. Это просто, что десериализованный john явно не тот же объект, который я создаю в тестовом классе?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...