3 votes

Le contrôleur REST de Spring se comporte différemment dans les tests unitaires

Le problème

Je suis nouveau dans le monde de Spring et j'essaie d'écrire quelques tests unitaires pour mon contrôleur REST. Je teste manuellement avec httpie o curl fonctionne bien, cependant, avec @WebMvcTest des choses étranges se produisent.

Voici ce qui se passe quand je PUT un nouvel utilisateur par 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"}

Comme vous pouvez le voir, l'en-tête Content-Type est présent dans la réponse et le corps est bien la nouvelle version de l'application User .

Voici comment j'essaie de tester la même chose automatiquement :

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

}

Mais ce que je comprends, c'est ça :

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

Que se passe-t-il ? Où est le corps de la MockHttpServletResponse ? Je dois manquer quelque chose, car il semble agir de manière complètement différente.


Autre code au cas où il serait nécessaire

Ma classe de contrôleur générique :

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 ... */

}

L'actuel User contrôleur :

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

MISE À JOUR 2020-04-22
Comme suggéré, j'ai retiré les génériques de l'équation, mais cela n'a pas aidé.

3voto

Ahmed Sayed Points 307

On dirait que le @WebMvcTest est de configurer une UserService qui utilise l'implémentation réelle, et votre bean est en quelque sorte ignoré.

Nous pouvons essayer de créer le UserService le haricot différemment

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

    ...
}

Vous pouvez également déplacer le stubbing vers une @Before dans vos tests

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

2voto

bertalanp99 Points 181

Apparemment, le problème venait de cette ligne :

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

Lorsque je change le premier john (qui est un User ) pour, par exemple any() le test passe très bien.


Quelqu'un pourrait-il m'éclairer sur la raison de cette situation ? Échanger john con any() fonctionne, mais donne l'impression de ne pas être à la hauteur. Le contrôleur passe le JSON désérialisé john à son service. Est-ce que c'est simplement que cet objet désérialisé john n'est évidemment pas le même objet que celui que je crée dans la classe de test ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X