spring-framework icon indicating copy to clipboard operation
spring-framework copied to clipboard

Support AssertJ variant in MockMvc [SPR-16637]

Open spring-projects-issues opened this issue 6 years ago • 25 comments

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

spring-projects-issues avatar Mar 23 '18 19:03 spring-projects-issues

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

ngeor avatar Jan 28 '19 20:01 ngeor

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.

manueljordan avatar May 26 '19 18:05 manueljordan

Hello @sbrannen here for your consideration. Thanks for your understanding

manueljordan avatar May 28 '19 01:05 manueljordan

+1 Support for AssertJ assertions would be great!

cassiomolin avatar Jul 10 '19 22:07 cassiomolin

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 like andExpect(T object, Consumer<T> consumer). Though perhaps something like andConsume() would make more sense for such a use case.
  • assertThat(list) is from AssertJ in this example.

sbrannen avatar Jul 18 '19 11:07 sbrannen

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.

sbrannen avatar Jul 18 '19 11:07 sbrannen

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.

philwebb avatar Jul 18 '19 12:07 philwebb

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.

sbrannen avatar Jul 18 '19 13:07 sbrannen

@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

123Haynes avatar Jul 18 '19 13:07 123Haynes

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.

sbrannen avatar Jul 18 '19 16:07 sbrannen

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.

123Haynes avatar Jul 18 '19 16:07 123Haynes

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

manueljordan avatar Jul 18 '19 20:07 manueljordan

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.

philwebb avatar Jul 18 '19 23:07 philwebb

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.

sbrannen avatar Jul 19 '19 14:07 sbrannen

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?

philwebb avatar Jul 20 '19 05:07 philwebb

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?

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.

sbrannen avatar Jul 20 '19 12:07 sbrannen

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:

  1. new MvcFluent API that declares a method with an AssertJ return type -- UriAssert just as a proof of concept
  2. new MvcFluent fluent() method in ResultActions
  3. new spring-test-test module that depends on spring-test and uses MockMvc but does not have a dependency on AssertJ
  4. MockMvcTests test case in the spring-test-test module that uses the fluent() API

Tests:

  • getPerson42(): uses MockMvc without fluent() API -- everything fine
  • fluentAndReturn(): uses MockMvc with the fluent() API but only invoking methods that do not return AssertJ types (i.e., andReturn()) -- everything fine
  • fluentAndAssertThat(): uses MockMvc with the fluent() 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".

sbrannen avatar Jul 20 '19 13:07 sbrannen

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?

Please note that in my proof of concept, MvcFluent does not extend an AssertJ interface. Rather, MvcFluent declares methods that have AssertJ return types.

sbrannen avatar Jul 20 '19 13:07 sbrannen

Regarding generic assertion hooks in MockMvc, see gh-23330.

sbrannen avatar Jul 22 '19 16:07 sbrannen

Will this be possible in both MockMvcResultMatchers and MockRestRequestMatchers? That would be amazing, the world is so tired of Hamcrest

nightswimmings avatar Dec 01 '19 12:12 nightswimmings

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.

sbrannen avatar Dec 02 '19 11:12 sbrannen

Are there any updates regarding assertj support?

ccostin93 avatar Sep 26 '20 19:09 ccostin93

Are there any updates regarding assertj support?

This is still in the 5.x Backlog for future consideration.

sbrannen avatar Sep 27 '20 12:09 sbrannen

are there any workarounds to use assertj or even an alternative to mockmvc that supports it?

mteodori avatar Jan 24 '21 19:01 mteodori

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.

sbrannen avatar Jan 25 '21 11:01 sbrannen

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.

... 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

soc avatar Aug 18 '24 15:08 soc