如何对HATEOAS及返回HATEOAS的控制器进行单元测试?Spring Boot中RepresentationModelAssembler测试与MockMvc JSON结构差异问题咨询
Hey there! Let's tackle your Spring HATEOAS testing questions step by step—they're common pain points, so I'm glad you asked.
1. Unit Testing HATEOAS Controllers & Core HATEOAS Behavior
When testing controllers that return HATEOAS responses, you’ll want to verify two key things: the business data is correct, and the hypermedia links are properly generated and formatted.
Validate Response Structure with MockMvc
UseMockMvcto send requests and assert on the JSON output, including the presence and correctness of links. For example:@Test void getUser_ReturnsHalFormattedResponse() throws Exception { mockMvc.perform(get("/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)) // Verify self link exists and has the right URL .andExpect(jsonPath("$._links.self.href").value("http://localhost/users/1")) // Verify a related resource link .andExpect(jsonPath("$._links.orders.href").value("http://localhost/users/1/orders")); }Test Link Generation in Isolation
If your link logic is complex (e.g., usingWebMvcLinkBuilderfor dynamic URLs), test it separately without spinning up the entire controller. This keeps tests fast and focused:@Test void generateSelfLink_ForUser_ReturnsCorrectUrl() { User user = new User(1L, "Jane Smith"); Link selfLink = WebMvcLinkBuilder.linkTo(UserController.class) .slash(user.getId()) .withSelfRel(); assertEquals("http://localhost/users/1", selfLink.getHref()); assertEquals(IanaLinkRelations.SELF, selfLink.getRel()); }
2. Unit Testing RepresentationModelAssembler Implementations
This is straightforward because RepresentationModelAssembler classes are plain Java components—no need for Spring context in unit tests. You can instantiate them directly (or use Mockito for dependencies) and verify their output.
Let’s say you have an assembler like this:
@Component public class UserModelAssembler implements RepresentationModelAssembler<User, UserModel> { @Override public UserModel toModel(User user) { UserModel model = UserModel.of(user); model.add(linkTo(UserController.class).slash(user.getId()).withSelfRel()); model.add(linkTo(methodOn(UserController.class).getUserOrders(user.getId())).withRel("orders")); return model; } }
Here’s how to test it:
class UserModelAssemblerTest { // No Spring needed—just instantiate the assembler private final UserModelAssembler assembler = new UserModelAssembler(); @Test void toModel_AddsCorrectLinksToUserModel() { // Given User testUser = new User(1L, "John Doe"); // When UserModel resultModel = assembler.toModel(testUser); // Then assertNotNull(resultModel); assertEquals(testUser.getId(), resultModel.getId()); // Validate self link Link selfLink = resultModel.getLink(IanaLinkRelations.SELF) .orElseThrow(() -> new AssertionError("Self link missing")); assertEquals("/users/1", selfLink.getHref()); assertEquals(IanaLinkRelations.SELF, selfLink.getRel()); // Validate orders link Link ordersLink = resultModel.getLink("orders") .orElseThrow(() -> new AssertionError("Orders link missing")); assertEquals("/users/1/orders", ordersLink.getHref()); assertEquals("orders", ordersLink.getRel()); } }
If your assembler has dependencies (e.g., a service to fetch related data), use Mockito to inject mocks:
class UserModelAssemblerTest { @Mock private UserOrderService orderService; @InjectMocks private UserModelAssembler assembler; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); } // Test methods can now mock orderService behavior as needed }
The difference you’re seeing comes down to how Spring HATEOAS handles JSON serialization:
Root Cause
- @SpringBootTest loads the full Spring context, including Spring HATEOAS’s auto-configured
HypermediaMappingJackson2HttpMessageConverter. This converter uses the HAL format, which serializes links to_linksand embedded content to_embedded. - StandaloneSetup (by default) uses Jackson’s default message converter, which serializes the
RepresentationModelclass’s rawlinksfield to alinksJSON key, andcontent(for paged/collections) to acontentkey—no HAL formatting.
Fixes to Match Production Output
Option 1: Add the HATEOAS Message Converter to StandaloneSetup
Manually configure MockMvc to use Spring HATEOAS’s converter:
@BeforeEach void setUp() { UserController controller = new UserController(new UserModelAssembler()); // Create the HAL-compatible message converter HypermediaMappingJackson2HttpMessageConverter halConverter = new HypermediaMappingJackson2HttpMessageConverter(); halConverter.setObjectMapper(new ObjectMapper().registerModule(new Jackson2HalModule())); mockMvc = MockMvcBuilders.standaloneSetup(controller) .setMessageConverters(halConverter) .build(); }
Option 2: Use @WebMvcTest Instead of StandaloneSetup
@WebMvcTest loads a slimmed-down web context that includes Spring HATEOAS’s auto-configuration, so MockMvc will automatically use HAL formatting. This is the simpler approach:
@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserModelAssembler assembler; @Test void getUser_ReturnsHalFormattedResponse() throws Exception { User testUser = new User(1L, "John Doe"); UserModel model = UserModel.of(testUser); model.add(Link.of("/users/1", IanaLinkRelations.SELF)); when(assembler.toModel(testUser)).thenReturn(model); mockMvc.perform(get("/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$._links.self.href").value("/users/1")); } }
内容的提问来源于stack exchange,提问作者AceCrow

