MockMvc wrapper allowing to easily test Spring HATEOAS HAL(-FORMS) endpoints.
- Add the
spring-boot-starterdependency:<dependency> <groupId>com.cosium.hal_mock_mvc</groupId> <artifactId>hal-mock-mvc-spring-boot-starter</artifactId> <version>${hal-mock-mvc.version}</version> <scope>test</scope> </dependency>
- Annotate your test class with
AutoConfigureHalMockMvcand injectHalMockMvc:@AutoConfigureHalMockMvc @SpringBootTest class MyTest { @Autowired private HalMockMvc halMockMvc; @Test void test() { halMockMvc .follow("current-user") .get() .andExpect(status().isOk()) .andExpect(jsonPath("$.alias").value("jdoe")); } }
Follow a single relation from the base URI:
halMockMvc
.follow("users")
.get()
.andExpect(status().isOk());Chain multiple hops to traverse deeper:
halMockMvc
.follow("users", "first")
.get()
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("jdoe"));Use Hop with URI template parameters:
halMockMvc
.follow(Hop.relation("file").withParameter("id", "foo"))
.get()
.andExpect(status().isOk());Shorthand methods are available directly on the traversal builder:
// GET
halMockMvc.follow("users").get()
.andExpect(status().isOk());
// POST with JSON body
halMockMvc.follow("users").post("{\"name\":\"john\"}")
.andExpect(status().isCreated());
// PUT with JSON body
halMockMvc.follow("user").put("{\"name\":\"jane\"}")
.andExpect(status().isNoContent());
// PATCH with JSON body
halMockMvc.follow("user").patch("{\"name\":\"jane\"}")
.andExpect(status().isNoContent());
// DELETE
halMockMvc.follow("user").delete()
.andExpect(status().isNoContent());Discover a template by key and submit it with raw JSON:
halMockMvc
.follow()
.templates()
.byKey("create")
.submit("{\"name\":\"john\"}")
.andExpect(status().isCreated());Submit a template with no body (e.g. DELETE affordance):
halMockMvc
.follow()
.templates()
.byKey("deleteByName")
.submit()
.andExpect(status().isNoContent());List all available templates:
Collection<Template> templates = halMockMvc
.follow()
.templates()
.list();
// Each Template exposes key() and representation()
// TemplateRepresentation exposes method(), contentType(), and target()Use createForm() on a template for typed, validated form population:
halMockMvc
.follow()
.templates()
.byKey("create")
.createForm()
.withString("name", "john")
.withInteger("age", 30)
.withBoolean("active", true)
.submit()
.andExpect(status().isCreated());Available typed methods: withString, withBoolean, withInteger, withLong, withDouble.
Collection variants: withStrings, withBooleans, withIntegers, withLongs, withDoubles.
createAndShift() submits, expects a 201 Created response, then starts a new traversal from the Location header:
halMockMvc
.follow()
.templates()
.byKey("create")
.createAndShift("{\"name\":\"john\"}")
.follow()
.get()
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("john"));submitAndExpect204NoContent() submits, expects 204 No Content, then resumes the traversal. This is useful for update-then-read flows:
halMockMvc
.follow()
.templates()
.byKey("create")
.createAndShift("{\"name\":\"john\"}")
.follow()
.templates()
.byKey("changeCity")
.submitAndExpect204NoContent("{\"city\":\"Casablanca\"}")
.follow()
.get()
.andExpect(status().isOk())
.andExpect(jsonPath("$.value").value("Casablanca"));Both methods are also available on the form builder (Form#createAndShift(), Form#submitAndExpectNoContent()).
Use multipartRequest() on the traversal builder:
byte[] fileContent = "hello".getBytes(StandardCharsets.UTF_8);
halMockMvc
.follow()
.multipartRequest()
.file("file", fileContent)
.put()
.andExpect(status().isNoContent());Use multipart() on a template:
halMockMvc
.follow(Hop.relation("file").withParameter("id", "foo"))
.templates()
.byKey("uploadFile")
.multipart()
.file("file", new byte[]{0})
.submit()
.andExpect(status().isNoContent());Template-based multipart also supports createAndShift():
halMockMvc
.follow()
.templates()
.byKey("addFile")
.multipart()
.file("file", new byte[]{0})
.createAndShift()
.follow()
.get()
.andExpect(status().isOk());Add RequestPostProcessor instances to the builder (e.g. for authentication):
HalMockMvc.builder(mockMvc)
.baseUri("/api")
.addRequestPostProcessor(request -> {
request.addHeader("Authorization", "Bearer my-token");
return request;
})
.build();Set default headers on the builder:
HalMockMvc.builder(mockMvc)
.baseUri("/api")
.header("X-Tenant-Id", "acme")
.build();When using the Spring Boot starter, register a HalMockMvcBuilderCustomizer bean to globally customize every HalMockMvc instance:
@TestConfiguration
class MyHalMockMvcConfig {
@Bean
HalMockMvcBuilderCustomizer securityCustomizer() {
return builder -> builder.addRequestPostProcessor(
SecurityMockMvcRequestPostProcessors.user("admin").roles("ADMIN")
);
}
}- Java 17+
- Spring dependencies matching Spring Boot 4 and above.
This project was created following spring-projects/spring-hateoas#733 discussion.