https://github.com/cosium/hal-mock-mvc
MockMvc wrapper allowing to easily test Spring HATEOAS HAL(-FORMS) endpoints
https://github.com/cosium/hal-mock-mvc
hal hal-form hateoas spring-hateoas
Last synced: 3 months ago
JSON representation
MockMvc wrapper allowing to easily test Spring HATEOAS HAL(-FORMS) endpoints
- Host: GitHub
- URL: https://github.com/cosium/hal-mock-mvc
- Owner: Cosium
- License: mit
- Created: 2023-05-20T08:44:52.000Z (about 3 years ago)
- Default Branch: master
- Last Pushed: 2025-02-11T09:03:27.000Z (over 1 year ago)
- Last Synced: 2025-02-25T20:41:18.616Z (over 1 year ago)
- Topics: hal, hal-form, hateoas, spring-hateoas
- Language: Java
- Homepage:
- Size: 88.9 KB
- Stars: 6
- Watchers: 4
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
[](https://github.com/Cosium/hal-mock-mvc/actions/workflows/ci.yml)
[](https://central.sonatype.com/artifact/com.cosium.hal_mock_mvc/hal-mock-mvc)
# HAL Mock MVC
MockMvc wrapper allowing to easily test [Spring HATEOAS](https://github.com/spring-projects/spring-hateoas) HAL(-FORMS) endpoints.
# Prerequisites
- Java 17+
- Spring dependencies matching Spring Boot 4 and above.
# Quick start
1. Add the `spring-boot-starter` dependency:
```xml
com.cosium.hal_mock_mvc
hal-mock-mvc-spring-boot-starter
${hal-mock-mvc.version}
test
```
2. Annotate your test class with `AutoConfigureHalMockMvc` and inject `HalMockMvc`:
```java
@AutoConfigureHalMockMvc
@SpringBootTest
class MyTest {
@Autowired
private HalMockMvc halMockMvc;
@Test
void test() {
halMockMvc
.follow("current-user")
.get()
.andExpect(status().isOk())
.andExpect(jsonPath("$.alias").value("jdoe"));
}
}
```
# Usage
## Following HAL links
Follow a single relation from the base URI:
```java
halMockMvc
.follow("users")
.get()
.andExpect(status().isOk());
```
Chain multiple hops to traverse deeper:
```java
halMockMvc
.follow("users", "first")
.get()
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("jdoe"));
```
Use `Hop` with URI template parameters:
```java
halMockMvc
.follow(Hop.relation("file").withParameter("id", "foo"))
.get()
.andExpect(status().isOk());
```
## HTTP methods
Shorthand methods are available directly on the traversal builder:
```java
// 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());
```
## HAL-FORMS templates
Discover a template by key and submit it with raw JSON:
```java
halMockMvc
.follow()
.templates()
.byKey("create")
.submit("{\"name\":\"john\"}")
.andExpect(status().isCreated());
```
Submit a template with no body (e.g. DELETE affordance):
```java
halMockMvc
.follow()
.templates()
.byKey("deleteByName")
.submit()
.andExpect(status().isNoContent());
```
List all available templates:
```java
Collection templates = halMockMvc
.follow()
.templates()
.list();
// Each Template exposes key() and representation()
// TemplateRepresentation exposes method(), contentType(), and target()
```
## Form builder
Use `createForm()` on a template for typed, validated form population:
```java
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`.
## Create and shift
`createAndShift()` submits, expects a `201 Created` response, then starts a new traversal from the `Location` header:
```java
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:
```java
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()`).
## Multipart requests
### Direct multipart
Use `multipartRequest()` on the traversal builder:
```java
byte[] fileContent = "hello".getBytes(StandardCharsets.UTF_8);
halMockMvc
.follow()
.multipartRequest()
.file("file", fileContent)
.put()
.andExpect(status().isNoContent());
```
### Template-based multipart
Use `multipart()` on a template:
```java
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()`:
```java
halMockMvc
.follow()
.templates()
.byKey("addFile")
.multipart()
.file("file", new byte[]{0})
.createAndShift()
.follow()
.get()
.andExpect(status().isOk());
```
## Request customization
### Request post-processors
Add `RequestPostProcessor` instances to the builder (e.g. for authentication):
```java
HalMockMvc.builder(mockMvc)
.baseUri("/api")
.addRequestPostProcessor(request -> {
request.addHeader("Authorization", "Bearer my-token");
return request;
})
.build();
```
### Custom headers
Set default headers on the builder:
```java
HalMockMvc.builder(mockMvc)
.baseUri("/api")
.header("X-Tenant-Id", "acme")
.build();
```
## Builder customizer (Spring Boot starter)
When using the Spring Boot starter, register a `HalMockMvcBuilderCustomizer` bean to globally customize every `HalMockMvc` instance:
```java
@TestConfiguration
class MyHalMockMvcConfig {
@Bean
HalMockMvcBuilderCustomizer securityCustomizer() {
return builder -> builder.addRequestPostProcessor(
SecurityMockMvcRequestPostProcessors.user("admin").roles("ADMIN")
);
}
}
```
# Genesis
This project was created following https://github.com/spring-projects/spring-hateoas/issues/733 discussion.