Skip to content

Calling build() twice on StandaloneMockMvcBuilder or RouterFunctionMockMvcBuilder fails #37622

Open

Description

@shinseongsu

Affects: main and 7.0.x. The cause has been in StandaloneMockMvcBuilder since at least 5.3.x.

Calling build() twice on a StandaloneMockMvcBuilder or RouterFunctionMockMvcBuilder fails on the second call:

StandaloneMockMvcBuilder builder = MockMvcBuilders.standaloneSetup(new HelloController());
builder.build();
builder.build();  // throws
org.springframework.context.ApplicationContextException: Cannot reinitialize with different application context:
current one is [...StubWebApplicationContext@79d63a4f], passed-in one is [...StubWebApplicationContext@2d74cbbd]
	at ...StandaloneMockMvcBuilder.initViewResolvers(StandaloneMockMvcBuilder.java:427)

This happens even if the test never configures a view resolver. initViewResolvers assigns the default resolver back to the builder field:

this.viewResolvers = (this.viewResolvers != null ? this.viewResolvers :
		Collections.singletonList(new InternalResourceViewResolver()));

On the second build(), that cached InternalResourceViewResolver is given a new StubWebApplicationContext, and ApplicationObjectSupport rejects it. RouterFunctionMockMvcBuilder has the same code.

Most tests build once, but RestTestClient makes the problem easier to hit. RestTestClient.bindToController(...) and bindToRouterFunction(...) build a new MockMvc on every build(), so this fails the same way:

RestTestClient.Builder<?> builder = RestTestClient.bindToController(new HelloController());
builder.build();
builder.build().get().uri("/").exchange().expectStatus().isOk();  // ApplicationContextException

Using a local variable in initViewResolvers in both builders, instead of assigning the field, fixes it. With that change, both snippets above pass, and so do the existing org.springframework.test.web.servlet.setup tests.

Related: built MockMvc instances share the builder's result matchers

Once the second build works, a second problem shows. AbstractMockMvcBuilder.build() passes its own globalResultMatchers and globalResultHandlers lists to the new MockMvc, and MockMvc stores them without copying. So alwaysExpect(...) / alwaysDo(...) calls made after build() also apply to MockMvc instances built earlier:

StandaloneMockMvcBuilder builder = MockMvcBuilders.standaloneSetup(new HelloController())
		.setViewResolvers((viewName, locale) -> null);  // works around the problem above
MockMvc first = builder.build();
builder.alwaysExpect(status().isNotFound()).build();

first.perform(get("/")).andExpect(status().isOk());
// AssertionError: Status expected:<404> but was:<200>

This is the same kind of problem as the recent HttpServiceProxyFactory.Builder fix (copy the lists in build()). build() also changes the shared defaultRequestBuilder in place. A MockMvcConfigurer's RequestPostProcessor is added to it with with(...) on every build, so the post-processors pile up.

If you agree these should be fixed, I'd like to submit a PR covering both builders with tests. Would you prefer the result matcher/handler copying in the same PR, or as a separate issue?

Activity

  1. Orcus01 commented on Nov 6, 2026

    @Orcus01

    I’d like to help reproduce this on main while it’s in triage. I can check two consecutive build() calls for both MockMvc builders and see whether the same failure occurs through RestTestClient. Would a minimal reproduction or focused regression test help with triage?

  2. shinseongsu commented on Nov 7, 2026

    @shinseongsu
    Author

    Thanks @Orcus01, that would help.

    A check on main for both builders, and through RestTestClient, would confirm the first part. This is the fix I have in mind:

    • initViewResolvers in StandaloneMockMvcBuilder and RouterFunctionMockMvcBuilder: keep the default resolver in a local variable instead of assigning it to the field.
    • AbstractMockMvcBuilder.build(): pass copies of globalResultMatchers and globalResultHandlers to MockMvc, as in the HttpServiceProxyFactory.Builder fix.

    The part I haven't settled is defaultRequestBuilder. build() adds the configurers' RequestPostProcessors to it in place, so they pile up across builds. I'm not sure yet whether to copy it in build() or to apply the post-processors to a fresh instance each time. If you notice something there, or another builder with the same pattern, please share it.

    I'll take care of the PR once the team agrees on the direction, so we don't end up with two PRs for the same change.

  3. Orcus01 commented on Nov 8, 2026

    @Orcus01

    I reproduced this on main at f95aca0 using JDK 25 and the Spring Gradle test task. In each case the first build() succeeded and the second threw ApplicationContextException: Cannot reinitialize with different application context:

    • MockMvcBuilders.standaloneSetup(controller)
    • MockMvcBuilders.routerFunctions(routes)
    • RestTestClient.bindToController(controller)
    • RestTestClient.bindToRouterFunction(routes)

    Each test used a fresh builder with the default view resolver and called build() twice on that same instance. The controller and router both expose a simple GET endpoint. The focused command was ./gradlew :spring-test:test --tests '*RepeatedMockMvcBuildTests'.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    in: testIssues in the test modulestatus: waiting-for-triageAn issue we've not yet triaged or decided on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions