spring-framework
spring-framework copied to clipboard
Support AssertJ variant in MockMvc [SPR-16637]
Brian Clozel opened SPR-16637 and commented
This Spring Boot issue shows that many developers would like to use AssertJ-style assertions with MockMvc. This would also be a nice complement to the existing Kotlin support, since Kotlin users seem to favor this style of assertions.
The linked Spring Boot issue contains a fairly advanced branch with a working prototype.
Reference URL: https://github.com/spring-projects/spring-boot/issues/5729
8 votes, 12 watchers
I've put together a library that offers AssertJ assertions for MockMvc
but also for ResponseEntity
(returned by TestRestTemplate
): https://github.com/ngeor/yak4j-spring-test-utils
I have read the comments in the other post (about SB), why not work around two branches? One for Hamcrest (Keeping the current available approach) and the other one for AssertJ ... Sadly Hamcrest is very verbose to test data about returned data either in XML or JSON.
Hello @sbrannen here for your consideration. Thanks for your understanding
+1 Support for AssertJ assertions would be great!
I've been thinking about this, and I'm not sure that we need to add explicit support for AssertJ in order to allow people to choose to use AssertJ, Truth, or any other assertion framework.
For example, if we introduce generic "consumer" support (as in Consumer<T>
), we might be able to do something like the following.
mockMvc.perform(get("/users"))
.andExpect(model().attribute("userList", List.class),
list -> assertThat(list).hasSize(2)));
- That
attribute()
method would have a signature like<T> T attribute(String name, Class<T> requiredType)
. - That
andExpect()
method would have a signature likeandExpect(T object, Consumer<T> consumer)
. Though perhaps something likeandConsume()
would make more sense for such a use case. -
assertThat(list)
is from AssertJ in this example.
A quick local spike with the following new method in ModelResultMatchers
...
public <T> ResultMatcher attribute(String name, Class<T> requiredType, Consumer<T> valueConsumer) {
return result -> {
ModelAndView mav = getModelAndView(result);
T value = (T) ClassUtils.resolvePrimitiveIfNecessary(requiredType).cast(mav.getModel().get(name));
valueConsumer.accept(value);
};
}
... results in the following working test case, modified from Spring's test suite.
@Test
public void testAttributeEqualTo() throws Exception {
mockMvc.perform(get("/"))
// Hamcrest
.andExpect(model().attribute("integer", equalTo(3)))
// AssertJ
.andExpect(model().attribute("integer", int.class, num -> assertThat(num).isEqualTo(3)));
}
The "consumer" variant is obviously considerably more verbose, but it shows that it's possible to provide such generic functionality with the current building blocks in MockMvc
.
That's good to know we could provide hooks. One of the more annoying aspects of trying to create this type of assertion is that you really need to expose AssertProvider
in your API. There's no easy way I found of making mockMvc
directly assertable. In my earlier spike I ended up creating a new MvcTester
class that was AssertJ specific. That makes the tests look nice but means it wont work without AssertJ on the classpath.
That makes the tests look nice but means it wont work without AssertJ on the classpath.
Indeed, it does look nice: fluent!
But I agree that forcing people to have AssertJ on the classpath is a major drawback. So, if we do anything AssertJ-specific, we should do our best to ensure that AssertJ remains an optional dependency.
@philwebb would it be possible to use a wrapper class that checks if AssertJ is available on the classpath and if not, falls back to hamcrest? That way AssertJ could remain optional.
Something like https://stackoverflow.com/questions/11432212/java-class-with-possibly-missing-run-time-dependencies
would it be possible to use a wrapper class that checks if AssertJ is available on the classpath and if not, falls back to hamcrest?
I'm not sure what you mean by falling back to Hamcrest.
The Hamcrest and AssertJ APIs are not compatible. So any explicit support for AssertJ would have to be in addition to the existing Hamcrest support.
I'm not sure what you mean by falling back to Hamcrest.
The Hamcrest and AssertJ APIs are not compatible. So any explicit support for AssertJ would have to be in addition to the existing Hamcrest support.
Sorry. Wrong wording. I was just trying to propose a possible solution that would keep AssertJ optional.
Hello Sam
The Hamcrest and AssertJ APIs are not compatible. So any explicit support for AssertJ would have to be in addition to the existing Hamcrest support.
Even when they are not compatible, seems an important goal is create a common API to change smoothly a technology to other quickly (i.e: Hamcrest to AssertJ) and let add a new technology (i.e:Truth or other in the future). Seems Consumer<T>
plays an important role.
You and Phill are the experts ...
If is not possible create a common API (seems is very hard for this situation) perhaps JUnit 5 would play an important role how a kind of Adapter
? I don't know, but the option to create a new module playing and bringing support how an adapter. Or consider create this adapter in Spring Test module
would it be possible to use a wrapper class that checks if AssertJ is available on the classpath and if not, falls back to hamcrest
Unfortunately not because we need a specific type available at compile time so that the IDE can offer code completion. I faced the same problem with JsonTester
in Spring Boot.
If we do anything AssertJ-specific, we should do our best to ensure that AssertJ remains an optional dependency.
That probably means we either need an AssertJ specific version of MockMvc, or we end up with a sub-par API. I'd probably prefer moving this back to Spring Boot if can't make the API fluent. We've already got much stronger opinions about using AssertJ so it might not feel so forced there.
That probably means we either need an AssertJ specific version of MockMvc, or we end up with a sub-par API.
Right. To achieve a decent API (fluent with code completion a la AssertJ's traditional style), we would either need a specific version of MockMvc
, or we would need to branch into a "parallel version" of the existing functionality that offers AssertJ APIs instead of Hamcrest.
For example, after the invocation of mockMvc.perform(get("/"))
, we could introduce a branch -- for example mockMvc.perform(get("/")).fluent()
, mockMvc.perform(get("/")).assertJ()
, or something similar -- and from that point on the API would be fluent and AssertJ-based. If the user never "branches", we hopefully should be able to make AssertJ an optional dependency.
I'd probably prefer moving this back to Spring Boot if can't make the API fluent. We've already got much stronger opinions about using AssertJ so it might not feel so forced there.
I still think that the "generic consumer" approach I outlined is worth considering, regardless of whether we support AssertJ directly in spring-test
.
For example, after the invocation of mockMvc.perform(get("/")), we could introduce a branch -- for example mockMvc.perform(get("/")).fluent(), mockMvc.perform(get("/")).assertJ(), or something
I'd need to brush up on my classloading knowledge. If fluent()
returns a MvcFluent
type that extends an AssertJ interface does that mean people can still call mockMvc.perform(get("/"))
if they don't have the AssertJ jar?
I'd need to brush up on my classloading knowledge. If
fluent()
returns aMvcFluent
type that extends an AssertJ interface does that mean people can still callmockMvc.perform(get("/"))
if they don't have the AssertJ jar?
You might be right: that might not work at all. I'd have to investigate. We do have similar "tricks" in various places in the framework where we work with an "optional" type only-on-demand, but the difference might be that we don't expose the "optional" type directly in those use cases. So I might be confusing those tricks with the scenario here.
The branching approach seems to work fine.
Check out the following feature branch to see it in action.
https://github.com/sbrannen/spring-framework/commits/issues/gh-21178-mock-mvc-assertj-playground
Overview:
- new
MvcFluent
API that declares a method with an AssertJ return type --UriAssert
just as a proof of concept - new
MvcFluent fluent()
method inResultActions
- new
spring-test-test
module that depends onspring-test
and usesMockMvc
but does not have a dependency on AssertJ -
MockMvcTests
test case in thespring-test-test
module that uses thefluent()
API
Tests:
-
getPerson42()
: usesMockMvc
withoutfluent()
API -- everything fine -
fluentAndReturn()
: usesMockMvc
with thefluent()
API but only invoking methods that do not return AssertJ types (i.e.,andReturn()
) -- everything fine -
fluentAndAssertThat()
: usesMockMvc
with thefluent()
API and invokes a method that returns an AssertJ type (i.e.,assertThat(URI)
) -- results in an compiler error as expected. For example, in Eclipse: "The method assertThat(URI) from the type MvcFluent refers to the missing type UriAssert".
If
fluent()
returns aMvcFluent
type that extends an AssertJ interface does that mean people can still callmockMvc.perform(get("/"))
if they don't have the AssertJ jar?
Please note that in my proof of concept, MvcFluent
does not extend an AssertJ interface. Rather, MvcFluent
declares methods that have AssertJ return types.
Regarding generic assertion hooks in MockMvc
, see gh-23330.
Will this be possible in both MockMvcResultMatchers and MockRestRequestMatchers? That would be amazing, the world is so tired of Hamcrest
Will this be possible in both MockMvcResultMatchers and MockRestRequestMatchers?
If we do something for MockMvc
, it should also be possible to do something similar for MockRestServiceServer
.
Are there any updates regarding assertj support?
Are there any updates regarding assertj support?
This is still in the 5.x Backlog for future consideration.
are there any workarounds to use assertj or even an alternative to mockmvc that supports it?
are there any workarounds to use assertj or even an alternative to mockmvc that supports it?
You might be happy with the MockMvcWebTestClient
variant of the WebTestClient
. It does not use AssertJ, but it does have a fluent API and supports MockMvc
under the hood.
are there any workarounds to use assertj or even an alternative to mockmvc that supports it?
You might be happy with the
MockMvcWebTestClient
variant of theWebTestClient
. It does not use AssertJ, but it does have a fluent API and supportsMockMvc
under the hood.
... which drags org.reactivestreams:reactive-streams
¹ in instead of Hamcrest.
So all this effort, and we kinda ended up where we started?!
EDIT: io.projectreactor
as well (java.lang.NoClassDefFoundError: reactor/core/scheduler/Schedulers
)
¹ new MockMvcHttpConnector
fails with java.lang.NoClassDefFoundError: org/reactivestreams/Publisher