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 Réponses :
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
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 code> 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.
Si LocalDateTime
n'est pas un problème, je pense que nous devrions implémenter equals
dans la classe User.
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()); } }
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éponseAvez-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 pasLocal *
pour autre chose que les calendriers des utilisateurs; utilisezInstant code > à 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.