Building RESTful APIs with HATEOAS (Hypermedia as the Engine of Application State) offers significant advantages in terms of discoverability and evolvability. However, verifying that your API correctly generates and serves these crucial links requires a robust integration testing strategy.
This article will guide you through the techniques and tools for writing effective integration tests for your HATEOAS-compliant Spring-based REST APIs, ensuring that clients can seamlessly navigate and interact with your resources.
Why Integration Test HATEOAS?
- Verify Link Generation: Integration tests ensure that the
_links
section in your responses is correctly populated with accuratehref
values and appropriaterel
attributes. - Test Navigation Paths: You can simulate client behavior by following the provided links to ensure that the API transitions between states as expected.
- Validate Link Semantics: Confirm that the
rel
values accurately describe the relationship between resources and the purpose of the links. - End-to-End Flow: Integration tests cover the entire request-response cycle, including how HATEOAS links are generated based on the current state and available actions.
Tools of the Trade
For integration testing Spring-based REST APIs, including those implementing HATEOAS, the following tools are invaluable:
- Spring Test (
MockMvc
): A powerful framework for testing Spring MVC controllers without starting a full server. It allows you to send mock HTTP requests and assert on the responses. - Spring REST Docs: While primarily for generating documentation, Spring REST Docs’ test infrastructure (
MockMvcRestDocumentation
) provides excellent support for capturing and asserting on hypermedia links. - AssertJ: A fluent assertion library that makes your tests more readable and expressive, especially when dealing with complex JSON structures.
- Hypermedia Assertions (from Spring HATEOAS Test): Spring HATEOAS provides dedicated assertion utilities specifically designed for verifying hypermedia links in your responses.
Strategies and Techniques
Here’s a breakdown of how to approach integration testing HATEOAS APIs:
1. Verifying Basic Links:
The fundamental step is to assert that the expected _links
section exists and contains the necessary links with correct href
values.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@SpringBootTest
@AutoConfigureMockMvc
public class ProductHateoasIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void getProductShouldIncludeSelfLink() throws Exception {
mockMvc.perform(get("/products/1")
.accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$._links.self.href").value("http://localhost:8080/products/1"));
}
@Test
void getProductShouldIncludeCategoryLink() throws Exception {
mockMvc.perform(get("/products/1")
.accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$._links.category.href").value("http://localhost:8080/categories/Electronics"));
}
}
- We use
MockMvc
to perform aGET
request to a product endpoint. - We specify
MediaTypes.HAL_JSON
in theAccept
header to ensure we receive a HATEOAS-compliant response. - We use
jsonPath
to assert the presence and value of specific links within the_links
section.
2. Using Spring HATEOAS Test Assertions:
Spring HATEOAS provides dedicated matchers for more expressive link assertions.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.hateoas.server.mvc.HypermediaLinkVerifier.verifyLink;
@SpringBootTest
@AutoConfigureMockMvc
public class ProductHateoasAdvancedIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void getProductShouldIncludeSelfLinkWithRel() throws Exception {
mockMvc.perform(get("/products/1")
.accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(content().string(verifyLink("self", linkTo(methodOn(ProductController.class).getProduct(1L)))));
}
@Test
void getProductShouldIncludeCategoryLinkWithRel() throws Exception {
mockMvc.perform(get("/products/1")
.accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(content().string(verifyLink("category", linkTo(methodOn(CategoryController.class).getCategory("Electronics")))));
}
}
verifyLink(String rel, Link expectedLink)
: This matcher checks if a link with the givenrel
exists and itshref
matches theLink
object constructed usingWebMvcLinkBuilder
. This approach is more robust as it ties the link generation to your controller methods.
3. Testing Collection Links and Pagination:
When dealing with collections, ensure that pagination links (next
, prev
, first
, last
) are correctly generated.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@SpringBootTest
@AutoConfigureMockMvc
public class ProductCollectionHateoasIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void getProductsShouldIncludeNextLink() throws Exception {
mockMvc.perform(get("/products?page=0&size=2")
.accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$._links.next.href").value("http://localhost:8080/products?page=1&size=2"));
}
@Test
void getProductsShouldIncludeSelfLinkWithPagination() throws Exception {
mockMvc.perform(get("/products?page=0&size=2")
.accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$._links.self.href").value("http://localhost:8080/products?page=0&size=2"));
}
}
4. Testing Action Links (e.g., update
, delete
):
Verify that links for performing actions are present and contain the correct href
and method
(if applicable).
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@SpringBootTest
@AutoConfigureMockMvc
public class ProductActionLinkIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void getProductShouldIncludeUpdateLink() throws Exception {
mockMvc.perform(get("/products/1")
.accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$._links.update.href").value("http://localhost:8080/products/1"))
.andExpect(jsonPath("$._links.update.templated").value(false)); // If not templated
}
@Test
void getProductShouldIncludeDeleteLink() throws Exception {
mockMvc.perform(get("/products/1")
.accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$._links.delete.href").value("http://localhost:8080/products/1"))
.andExpect(jsonPath("$._links.delete.templated").value(false)); // If not templated
}
}
5. Simulating Client Navigation (Advanced):
For more complex scenarios, you can write tests that simulate a client navigating the API by extracting links from responses and making subsequent requests.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@SpringBootTest
@AutoConfigureMockMvc
public class HateoasNavigationIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void canNavigateFromProductToCategory() throws Exception {
ResultActions productResult = mockMvc.perform(get("/products/1")
.accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$._links.category.href").exists());
String categoryUri = jsonPath("$._links.category.href").evaluate(productResult.andReturn());
mockMvc.perform(get(categoryUri)
.accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Electronics"));
}
}
6. Integrating with Spring REST Docs:
Spring REST Docs allows you to document your HATEOAS links within your API documentation.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs(outputDir = "build/generated-snippets")
public class ProductHateoasRestDocsIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void getProductShouldDocumentHateoasLinks() throws Exception {
mockMvc.perform(get("/products/1")
.accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andDo(document("product-get",
responseFields(
fieldWithPath("id").description("The product ID"),
fieldWithPath("name").description("The product name"),
fieldWithPath("price").description("The product price"),
// ... other fields
fieldWithPath("_links").description("HATEOAS links")
),
links(
linkWithRel("self").description("Link to this product"),
linkWithRel("category").description("Link to the product's category")
// ... other links
)
));
}
}
Best Practices for Testing HATEOAS:
- Focus on Link Presence and Structure: Ensure the essential links are present and follow the expected naming conventions.
- Validate
rel
Attributes: Verify that therel
values accurately describe the link’s purpose. - Test Key Navigation Paths: Simulate how clients would typically navigate the API to perform common tasks.
- Use Dedicated HATEOAS Assertion Libraries: Leverage the utilities provided by Spring HATEOAS Test for more expressive and robust assertions.
- Integrate with API Documentation: Use tools like Spring REST Docs to document your HATEOAS links alongside your API endpoints and data structures.
- Consider Different Resource States: Test how the available links change based on the state of the resource.
Conclusion:
Thorough integration testing of your HATEOAS-compliant APIs is crucial for ensuring that clients can effectively discover and interact with your resources. By utilizing tools like MockMvc
, AssertJ, and the hypermedia assertions from Spring HATEOAS Test, you can write comprehensive tests that validate the correctness and navigability of your API’s hypermedia links, leading to more robust and evolvable RESTful services.
Discover more from GhostProgrammer - Jeff Miller
Subscribe to get the latest posts sent to your email.