1
votes

Spring Boot Test: UserControllerTest échoue lors de l'utilisation de Jackson ObjectMapper pour convertir le modèle en chaîne JSON

Je teste un @RestContoller dans Spring Boot qui a une méthode @PostMapping et la méthode @RequestBody est validée à l'aide de Annotation @Valid . Pour le tester, j'utilise MockMvc et afin de remplir le contenu du corps de la requête, j'utilise Jackson ObjectMapper ; cependant, lorsque le modèle est réussi, le test échoue:

String json = "{\n" +
                "\t\"firstName\" : \"John\",\n" +
                "\t\"lastName\" : \"QPublic\",\n" +
                "\t\"password\" : \"123456789\",\n" +
                "\t\"createdAt\" : \"2016-11-09T11:44:44.797\",\n" +
                "\t\"updatedAt\" : \"2016-11-09T11:44:44.797\",\n" +
                "\t\"emailAddress\" : \"john.public@gmail.com\"\n" +
                "}";

        mockMvc.perform(MockMvcRequestBuilders.post("/api/user/register")
                .content(json)
                .contentType(MediaType.APPLICATION_JSON)
                )
                .andExpect(MockMvcResultMatchers.status().isOk());

Modèle utilisateur:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc(addFilters = false)
public class UserControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void whenRequestValid_thenReturnStatusOk() throws Exception{
        User user = new User("John", "QPublic", "john.public@gmail.com",
                "123456789", LocalDateTime.now(), LocalDateTime.now());      
        mockMvc.perform(MockMvcRequestBuilders.post("/api/user/register")
                .content(new ObjectMapper().writeValueAsString(user))
                .contentType(MediaType.APPLICATION_JSON)
                )
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}

UserController:

@RestController
@RequestMapping("/api/user")
public class UserController {

    @Autowired
    UserService userService;

    @PostMapping("/register")
    public ResponseEntity<String> register(@RequestBody @Valid User user){
        userService.createUser(user);
        return ResponseEntity.ok().build();
    }
}

UserControllerTest:

@Entity
@Table(name = "users",
        uniqueConstraints = @UniqueConstraint(columnNames = {"EMAIL"}))
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private long id;

@Column(name = "FIRST_NAME")
@NotNull
private String firstName;

@Column(name = "LAST_NAME")
@NotNull
private String lastName;

@Column(name = "EMAIL")
@NotNull
@Email
private String emailAddress;

@Column(name = "PASSWORD")
@NotNull
private String password;

@Column(name = "CREATED_AT")
@NotNull
@Convert(converter = LocalDateTimeConveter.class)
private LocalDateTime createdAt;

@Column(name = "UPDATED_AT")
@NotNull
@Convert(converter = LocalDateTimeConveter.class)
private LocalDateTime updatedAt;

public User(@NotNull String firstName, @NotNull String lastName,
                @NotNull @Email String emailAddress, @NotNull String password,
                @NotNull LocalDateTime createdAt, @NotNull LocalDateTime updatedAt) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.emailAddress = emailAddress;
        this.password = password;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
    }

//setters and getters: omitted

Lorsque je crée une chaîne JSON manuellement, le test réussit:

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /api/user/register
       Parameters = {}
          Headers = [Content-Type:"application/json"]
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = com.springboottutorial.todoapp.controller.UserController
           Method = public org.springframework.http.ResponseEntity<java.lang.String> com.springboottutorial.todoapp.controller.UserController.register(com.springboottutorial.todoapp.dao.model.User)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = org.springframework.http.converter.HttpMessageNotReadableException

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

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 400
    Error message = null
          Headers = []
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

java.lang.AssertionError: Status 
Expected :200
Actual   :400

p>


4 commentaires

Avez-vous exécuté l'application pour vérifier si le point de terminaison fonctionne avec Postman ou un autre outil similaire?. Veuillez inclure votre pom.xml dans votre réponse


Avez-vous essayé d'imprimer la chaîne renvoyée par writeValueAsString pour la valider? (Je soupçonne très fortement que vous obtenez un format "horodatage", et n'utilisez pas Local * pour autre chose que les calendriers des utilisateurs; utilisez Instant à la place.)


@EduardoEljaiek Oui, il fonctionne correctement lorsque j'utilise l'outil Postman.


@chrylis Merci pour votre suggestion! LocalDateTime analysé en jours, mois, années lorsqu'il est mappé à la chaîne JSON afin que le conflit se produise. J'ai rendu LocalDateTime nullable et cette fois, cela a fonctionné. J'essaie de trouver une solution solide et d'afficher la réponse.


4 Réponses :


0
votes

C'est ainsi que j'implémente mon test de contrôleurs de repos. Peut-être que cela pourrait vous aider.

J'ai cette classe abstraite pour encapsuler les fonctionnalités de tests courantes concernant le mappage JSON .

    @WebMvcTest(UserController.class)
    @AutoConfigureMockMvc(addFilters = false)
    public class UserControllerTest extends RestControllerTest {

        @Autowired
        MockMvc mockMvc;

        @Test
        public void whenRequestValid_thenReturnStatusOk() throws Exception{
            User user = new User("John", "QPublic", "john.public@gmail.com",
                    "123456789", LocalDateTime.now(), LocalDateTime.now());      
            mockMvc.perform(MockMvcRequestBuilders.post("/api/user/register")
                    .content(toJson(user))
                    .contentType(MediaType.APPLICATION_JSON)
                    )
                    .andExpect(MockMvcResultMatchers.status().isOk());
        }
    }

Vous pourriez utilisez-le dans votre classe de test comme ceci

    import lombok.SneakyThrows;
    import lombok.val;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.MediaType;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
    import org.springframework.mock.http.MockHttpOutputMessage;

    import java.nio.charset.StandardCharsets;
    import java.util.Arrays;

    import static org.junit.Assert.assertNotNull;

    public abstract class RestControllerTest {

        private final MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
                MediaType.APPLICATION_JSON.getSubtype(),
                StandardCharsets.UTF_8);

        private HttpMessageConverter messageConverter;

        protected MediaType getContentType() {
            return contentType;
        }

        @Autowired
        void setConverters(HttpMessageConverter<?>[] converters) {
            messageConverter = Arrays.stream(converters)
                    .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter)
                    .findAny()
                    .orElse(null);

            assertNotNull("the JSON message converter must not be null",
                    messageConverter);
        }

        @SneakyThrows
        protected String toJson(Object data) {
            val mockHttpOutputMessage = new MockHttpOutputMessage();
            messageConverter.write(
                    data, MediaType.APPLICATION_JSON, mockHttpOutputMessage);
            return mockHttpOutputMessage.getBodyAsString();
        }
    }

J'espère que cela fonctionne pour vous


0 commentaires

0
votes

Comme Chrylis l'a mentionné dans les commentaires, le problème est survenu en raison de conflits de sérialisation de l'API Java 8 Date & Time et de Jackson. Par défaut, ObjectMapper ne comprend pas le type de données LocalDateTime , je dois donc ajouter com.fasterxml.jackson.datatype: jackson-datatype-jsr310 dépendance à mon maven qui est un module de type de données pour que Jackson reconnaisse les types de données API Java 8 Date & Time. Une question stackoverflow et une article de blog m'a aidé pour découvrir quel était réellement mon problème.


0 commentaires

0
votes

Si LocalDateTime n'est pas un problème, je pense que nous devrions implémenter equals dans la classe User.


0 commentaires

1
votes

Spring ne vous fournit pas nécessairement une instance ObjectMapper "vanille". En demandant à Spring d'injecter l'instance ObjectMapper dans le test au lieu de créer un ObjectMapper avec le constructeur par défaut, vous obtiendrez une instance qui correspond à votre environnement d'exécution réel, à condition que votre profil Spring pour vos tests unitaires soit configuré correctement.

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc(addFilters = false)
public class UserControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    public void whenRequestValid_thenReturnStatusOk() throws Exception{
        User user = new User("John", "QPublic", "john.public@gmail.com",
                "123456789", LocalDateTime.now(), LocalDateTime.now());      
        mockMvc.perform(MockMvcRequestBuilders.post("/api/user/register")
                .content(objectMapper.writeValueAsString(user))
                .contentType(MediaType.APPLICATION_JSON)
                )
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}


0 commentaires