Assume that we have a versioned REST API:

@RestController
@RequestMapping("/api")
class QuoteController {

    @GetMapping(path = "/quote", produces = "application/json", version = "1.0+")
    ResponseEntity<Quote> getQuote() {
       //...
    }

    @GetMapping(path = "/quote", produces = "application/json", version = "2.0")
    ResponseEntity<List<Quote>> getQuotes() {
       //...
    }

It is configured like this:

spring:
  mvc:
    apiversion:
      use:
        header: API-Version

To test it properly, we need to invoke all endpoints with a proper version header:

        mockMvc.perform(get("/api/quote")
                        .accept(MediaType.APPLICATION_JSON)
                        .header("API-Version", "1.0")
                )
                .andExpect(status().isOk())

With Spring 7, we can use apiVersion method, which is more convenient and explicit

        mockMvc.perform(get("/api/quote")
                        .accept(MediaType.APPLICATION_JSON)
                        .apiVersion("1.0")
                )
                .andExpect(status().isOk())

The problem is that this approach doesn't work by default, error No ApiVersionInserter appears:

[ERROR] com.test.boot4.server.quote.QuoteControllerTest.getQuote_returnsJsonFromService -- Time elapsed: 0.025 s <<< ERROR!
java.lang.IllegalStateException: No ApiVersionInserter
        at org.springframework.util.Assert.state(Assert.java:80)
        at org.springframework.test.web.servlet.request.AbstractMockHttpServletRequestBuilder.buildRequest(AbstractMockHttpServletRequestBuilder.java:751)
        at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:170)

To fix this, we need to add a bit of boilerplate:

    @TestConfiguration
    static class QuoteControllerAddTestConfig implements MockMvcBuilderCustomizer {
        @Override
        public void customize(ConfigurableMockMvcBuilder<?> builder) {
            builder.apiVersionInserter(ApiVersionInserter.useHeader("API-Version"));
        }
    }

Suggestion:

Is it possible to perform this customization in a auto-configurer based on the application configuration? Basically, all the information we need is there, so why don't we reuse it?

Comment From: bclozel

I discussed this yesterday with @snicoll and we found several reasons not to apply this automatically. Conceptually, this configuration is applied to the REST controller endpoints when a version is used for mapping purposes. The client can have a different configuration, for many reasons.

  • there are many endpoints that clients might want to test where versioning does not apply (non-versioned REST endpoints, views, resource handling)
  • developers might have different testing strategies in their test suites: not specifying a version and using the default version implicitly, only using a version for some endpoints. This really depends on how developers intend to evolve their APIs and test for backwards compatibility
  • custom insertion strategies would not work

We'll discuss that as a team and reconsider our initial position.

Comment From: snicoll

On top of what Brian said, I disagree with

Is it possible to perform this customization in a auto-configurer based on the application configuration? Basically, all the information we need is there, so why don't we reuse it?

That's a bit of a shortcut. There are other ways that you can configure the ApiVersionInserter. You could have a WebMvcConfigurer that configures something programmatically for instance. In fact this might be the main use case for those who have several things to configure.

As such, us doing something that can be inaccurate (or incomplete) is worse than the status quo.

Comment From: belyaev-andrey

My impression was that ApiVersioningInserter applies only if I invoke .apiVersion("1.0") method. It means that the code for non-versioned endpoints should stay the same and won't be affected.

As for custom strategies - it is fair point, but I'm not saying we should get rid of programmatic configuration. If there is a programmatic configuration, we should override properties.

Basically, when we're dealing with testing versioned API, we need either: * Copy-paste this boilerplate configuration code from other projects (nobody remembers it) * Add versioning parameter manually to the request (I guess, it's going to be the most popular case)

I might definitely miss something, but it would be so "magical" if versioned API testing work out-of-the-box without any boilerplate code as soon as it is configured in the app.

I'm also OK with a special configuraion properties for MockMvcBuilderCustomizer, just to avoid writing code.

Comment From: snicoll

As for custom strategies - it is fair point, but I'm not saying we should get rid of programmatic configuration. If there is a programmatic configuration, we should override properties.

who is "we"? Technically you could set the property and have a configurer that overrides it. If "we" is the user then we can auto-configure something we shouldn't and break them. If "we" is us, then we have no way to know what was configured programmatically as we get a ApiVersionStrategy that doesn't expose that information (and it should not).

I might definitely miss something, but it would be so "magical" if versioned API testing work out-of-the-box without any boilerplate code as soon as it is configured in the app.

See above, and let me know if I've missed something.

I'm also OK with a special configuraion properties for MockMvcBuilderCustomizer, just to avoid writing code.

Customizer don't have configuration properties. I suppose you mean an attribute on @AutoConfigureMockMvc? What would it look like considering there are four ways to specify the version at the moment.

Comment From: belyaev-andrey

After the second consideration I tend to agree with you. Maybe it is not feasible to do this and it adds more confusion. I believe there is a way to simplify test configuration, but I cannot imagine a better approach now. At the moment, a section in the documentation should be enough I guess. Thanks!

Comment From: snicoll

Thanks for following-up. I think we should at least explore ways to make the setup simpler.

Comment From: snicoll

@rstoyanchev and I brainstormed and https://github.com/spring-projects/spring-framework/issues/35565 provides a shortcut in terms of programmatic API. We'll use this issue to document what the options are to specify the strategy.

For MockMvc, I'd envision something like this:

@Configuration(proxyBeanMethods = false)
class ApiVersioningTestConfig {

    @Bean
    public MockMvcBuilderCustomizer apiVersioningMockMvcCustomizer() {
        return (builder) -> builder.apiVersionInserter(apiVersionBuilder -> apiVersionBuilder.useHeader("API-Version"));
    }

}

How such a test class will be picked up is still TBD.

We've also discussed the use case you've reported here. For that to work we'd need to retrieve the server-side configuration for API versioning (that's doable) and then we'd need to translate it so that it matches what the client expects in terms of configuration. While it might work now, it's certainly restricting our ability to expand the server-side API in ways that makes it impossible to automatically translate the client-side strategy. We also believe that auto-configuring such strategy can lead to confusing situation and we think it is better as an opt-in option.

Comment From: belyaev-andrey

Thanks! Have you considered configuring MockMvc using its own properties set similar to the existing properties for restclient? So we can create something like this in the app-test.yaml:

spring:
  test:
    mockmvc:
      apiversion:
        use:
          header: API-Version

Anyway, I trust your decision. If it is hard to do it in a simple way and avoid confusion you can close the issue or put it to the backlog.

Comment From: snicoll

No I did not. The problem here is that you may have many clients that could use that stuff not only mockmvc.

I think we need to get more feedback before changing anything.

We won't close this issue but we'll use it to improve the documentation.

Comment From: belyaev-andrey

Thank you for your time! Very appreciated!