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.xmldans votre réponseAvez-vous essayé d'imprimer la chaîne renvoyée par
writeValueAsStringpour 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.