Overview

As discussed in https://github.com/spring-projects/spring-framework/issues/21178#issuecomment-512774918, it would be possible to provide generic hooks in MockMvc APIs that allow one to use any assertion library of choice. Such generic hooks could even be used for purposes other than performing assertions.

Due to the usefulness of such general purpose hooks, we should consider introducing them in various MockMvc APIs even if we do not provide specific support for a fluent assertion API such as AssertJ (see gh-21178).

The introduction of such hooks would also align with similar support provided in WebTestClient -- for example, WebTestClient.BodySpec.value(Consumer<B>).

Deliverables

TBD

Comment From: simonbasle

This could be implemented as a satisfies(Consumer<B>) method on various xxxResultMatchers classes of the MockMvc hierarchy. The naming is borrowed from AssertJ's own similar consumer-based assertion.

for instance, to assert/consume status codes, we'd add this to StatusResultMatchers:

public ResultMatcher satisfies(IntConsumer satisfyConsumer) {
    return result -> satisfyConsumer.accept(result.getResponse().getStatus());
}

usage example with AssertJ:

this.mockMvc.perform(post("/persons"))
    .andExpect(status().satisfies(code -> Assertions.assertThat(code).isBetween(100, 300)));

The relevant resultmatchers seems to be most classes in org.springframework.test.web.servlet.result: - ContentResultMatchers - CookieResultMatchers - FlashAttributeResultMatchers - HandlerResultMatchers - HeaderResultMatchers - JsonPathResultMatchers - ModelResultMatchers - RequestResultMatchers - StatusResultMatchers - ViewResultMatchers - XpathResultMatchers

Comment From: simonbasle

some of these seem to have multiple "target" that they internally use. eg. StatusResultMatchers targets primarily result.getResponse().getStatus() but also has a few methods targeting result.getResponse().getErrorMessage()... 🤔

Comment From: sbrannen

some of these seem to have multiple "target" that they internally use. eg. StatusResultMatchers targets primarily result.getResponse().getStatus() but also has a few methods targeting result.getResponse().getErrorMessage()... 🤔

Indeed. The single satisfies(Consumer<B>) method approach seems quite elegant until you run into collisions for the same type (e.g., String for character encoding vs. expected content in ContentResultMatchers).

Thanks for brainstorming though! 👍

It's good to toss around a few ideas.

Comment From: sbrannen

Though, perhaps it's not so bad to introduce sibling consumer methods like:

  • StatusResultMatchers#codeSatisfies(IntConsumer action)
  • status().codeSatisfies(code -> assertThat(code).isEqualTo(200))
  • or perhaps better: status().code(code -> assertThat(code).isEqualTo(200))
  • StatusResultMatchers#reasonSatisfies(Consumer<String> action)
  • status().reasonSatisfies(reason -> assertThat(reason).isEqualTo("Expired token"))
  • or perhaps better: status().reason(reason -> assertThat(reason).isEqualTo("Expired token"))

or similarly:

  • ContentResultMatchers#encoding(Consumer<String> action)
  • content().encoding(encoding -> assertThat(encoding).isEqualTo("UTF-8"))
  • ContentResultMatchers#string(Consumer<String> action)
  • content().string(body -> assertThat(body).contains("hello"))

@simonbasle & @rstoyanchev, thoughts?

Comment From: nightswimmings

I like more the Satisfies approach, even though is much more verbose, it is clear in its intention (just a personal opinion)

Comment From: sbrannen

I like more the Satisfies approach, even though is much more verbose, it is clear in its intention

Do you mean the variants like the following?

  • status().codeSatisfies(code -> assertThat(code).isEqualTo(200))

Comment From: sbrannen

@bclozel and @snicoll, would you consider this superseded by your work on #21178?

Comment From: bclozel

To me this looks like a duplicate of #21178 in the first place, so yes let's close it.