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 accurate href values and appropriate rel 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 a GET request to a product endpoint.
  • We specify MediaTypes.HAL_JSON in the Accept 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 given rel exists and its href matches the Link object constructed using WebMvcLinkBuilder. 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 the rel 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.

By Jeffery Miller

I am known for being able to quickly decipher difficult problems to assist development teams in producing a solution. I have been called upon to be the Team Lead for multiple large-scale projects. I have a keen interest in learning new technologies, always ready for a new challenge.