You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多Spring Boot项目集成测试:多WebApplicationContext配置咨询

Great question—this is such a common pain point with legacy distributed Java apps, and your initial idea has real merit. Let’s break down how to make it work smoothly, plus some simpler alternatives if loading 6 separate web contexts feels too heavy.


方案1:合并Spring上下文(你的初始思路,调整后可行)

If all 6 projects are Spring Boot apps, you can create a test-only config class to import all their main application classes, merging them into a single Spring context. The main gotcha here is bean conflicts (e.g., identically named beans across projects)—super common in legacy codebases, so you’ll need to handle those explicitly.

Step-by-Step Setup:

  1. Add all 6 projects as dependencies to your test project (either as modules or JARs).
  2. Create a combined config class to import all app contexts:
@Configuration
@Import({
    Project1Application.class,
    Project2Application.class,
    Project3Application.class,
    // ... add the remaining 3 project main classes
})
public class CombinedTestConfig {
    // Resolve bean conflicts here if needed:
    // Use @Primary to prioritize one bean over another, or @Qualifier to distinguish them
    @Primary
    @Bean
    public UserService userService() {
        return new Project1UserService(); // Example: pick which UserService to use
    }
}
  1. Write tests that use this combined context, and create project-specific MockMvc instances:
@SpringBootTest(classes = CombinedTestConfig.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class FullSystemIntegrationTest {

    @Autowired
    private WebApplicationContext sharedContext;

    private MockMvc project1MockMvc;
    private MockMvc project2MockMvc;

    @BeforeEach
    void setUp() {
        // Create MockMvc for Project1 (filter requests by its context path)
        project1MockMvc = MockMvcBuilders.webAppContextSetup(sharedContext)
                .addFilter((req, res, chain) -> {
                    if (req.getRequestURI().startsWith("/project1/")) {
                        chain.doFilter(req, res);
                    } else {
                        res.sendError(HttpStatus.NOT_FOUND.value());
                    }
                })
                .build();

        // Repeat for Project2 and others
        project2MockMvc = MockMvcBuilders.webAppContextSetup(sharedContext)
                .addFilter((req, res, chain) -> {
                    if (req.getRequestURI().startsWith("/project2/")) {
                        chain.doFilter(req, res);
                    } else {
                        res.sendError(HttpStatus.NOT_FOUND.value());
                    }
                })
                .build();
    }

    @Test
    void testProject1TriggersProject2Workflow() throws Exception {
        // Simulate a request to Project1 that calls Project2 internally
        project1MockMvc.perform(post("/project1/api/trigger-workflow"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value("COMPLETED"));

        // Verify Project2's controller was invoked (use Mockito.verify or database assertions)
    }
}

方案2:拦截服务间 HTTP 请求(更轻量,避免上下文合并)

Instead of loading 6 contexts, intercept all outgoing HTTP calls (originally targeting your dev server) and forward them to local controller instances. This is ideal if bean conflicts make merging contexts impractical.

Option A: Use Spring’s MockRestServiceServer (for RestTemplate)

If your projects use RestTemplate for service calls, this built-in tool lets you intercept requests and route them to local MockMvc:

@SpringBootTest(classes = Project1Application.class)
public class Project1ToProject2Test {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private MockMvc project2MockMvc; // Import Project2's controllers into the test context

    private MockRestServiceServer mockServer;

    @BeforeEach
    void setUp() {
        mockServer = MockRestServiceServer.createServer(restTemplate);

        // Intercept calls to Project2's dev server URL, forward to local MockMvc
        mockServer.expect(requestTo("http://dev-server/project2/api/get-data"))
                .andRespond(request -> {
                    // Build a MockMvc request matching the incoming call
                    RequestBuilder mockMvcRequest = MockMvcRequestBuilders.get("/api/get-data")
                            .params(request.getParameters())
                            .contentType(MediaType.APPLICATION_JSON);

                    // Execute the request against local Project2 controllers
                    MvcResult result = project2MockMvc.perform(mockMvcRequest).andReturn();
                    org.springframework.mock.web.MockHttpServletResponse springResponse = result.getResponse();

                    // Convert the result back to a MockRestServiceServer response
                    return MockRestResponseCreators.withSuccess()
                            .body(springResponse.getContentAsString())
                            .contentType(MediaType.APPLICATION_JSON)
                            .createResponse(request);
                });
    }

    @Test
    void testProject1CallsProject2() throws Exception {
        project1MockMvc.perform(get("/project1/api/fetch-external-data"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").value("local-test-value"));

        // Verify the intercepted request was triggered
        mockServer.verify();
    }
}

Option B: Use WireMock (for complex HTTP clients)

If your projects use WebClient or other non-RestTemplate HTTP tools, WireMock acts as a reverse proxy to forward requests to local controllers:

  1. Add WireMock to your test dependencies:
<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8</artifactId>
    <scope>test</scope>
</dependency>
  1. Configure WireMock to forward requests:
@SpringBootTest(classes = Project1Application.class)
public class WireMockForwardTest {

    @Autowired
    private MockMvc project2MockMvc;

    private WireMockServer wireMockServer;

    @BeforeEach
    void setUp() {
        // Start WireMock on port 8080 (update your project's service URLs to point here)
        wireMockServer = new WireMockServer(8080);
        wireMockServer.start();

        // Forward all Project2 requests to local MockMvc
        wireMockServer.stubFor(get(urlMatching("/project2/api/.*"))
                .willReturn(aResponse()
                        .withTransformer(new MockMvcForwardTransformer(project2MockMvc))));
    }

    @AfterEach
    void tearDown() {
        wireMockServer.stop();
    }

    // Custom transformer to route WireMock requests to MockMvc
    static class MockMvcForwardTransformer extends ResponseTransformer {
        private final MockMvc mockMvc;

        public MockMvcForwardTransformer(MockMvc mockMvc) {
            this.mockMvc = mockMvc;
        }

        @Override
        public Response transform(Request request, Response response, FileSource files) {
            try {
                // Map WireMock request to MockMvc request
                RequestBuilder mockMvcRequest = MockMvcRequestBuilders.request(
                        request.getMethod().name(),
                        request.getUrl().replace("/project2", "")
                    )
                    .content(request.getBodyAsString())
                    .contentType(MediaType.APPLICATION_JSON)
                    .headers(convertWireMockHeadersToSpring(request.getHeaders()));

                // Execute and convert response
                MvcResult result = mockMvc.perform(mockMvcRequest).andReturn();
                org.springframework.mock.web.MockHttpServletResponse springResponse = result.getResponse();

                return Response.response()
                        .status(springResponse.getStatus())
                        .body(springResponse.getContentAsString())
                        .headers(convertSpringHeadersToWireMock(springResponse))
                        .build();
            } catch (Exception e) {
                return Response.response().status(500).body(e.getMessage()).build();
            }
        }

        @Override
        public String getName() {
            return "mock-mvc-forward";
        }

        // Helper methods to convert headers between WireMock and Spring formats
        private HttpHeaders convertWireMockHeadersToSpring(com.github.tomakehurst.wiremock.http.HttpHeaders wireMockHeaders) {
            HttpHeaders springHeaders = new HttpHeaders();
            wireMockHeaders.all().forEach(header -> springHeaders.add(header.key(), header.values()));
            return springHeaders;
        }

        private com.github.tomakehurst.wiremock.http.HttpHeaders convertSpringHeadersToWireMock(org.springframework.mock.web.MockHttpServletResponse springResponse) {
            com.github.tomakehurst.wiremock.http.HttpHeaders wireMockHeaders = new com.github.tomakehurst.wiremock.http.HttpHeaders();
            springResponse.getHeaderNames().forEach(header -> wireMockHeaders.addHeader(header, springResponse.getHeader(header)));
            return wireMockHeaders;
        }
    }
}

方案3: Handle MQ Communication

Don’t forget the MQ part of your system! Here’s how to cover that:

  • For Kafka: Use @EmbeddedKafka to spin up a local Kafka server, and update all project configs to point to it. Messages will flow locally instead of to a remote broker.
  • For RabbitMQ: Use @RabbitTest or an embedded RabbitMQ server.
  • Simple Mocking: Use @MockBean to mock MQ producers/consumers for fast, isolated tests:
@SpringBootTest
public class MQWorkflowTest {

    @MockBean
    private Project1MQProducer mqProducer;

    @Autowired
    private Project2MQConsumer mqConsumer;

    @Test
    void testMQMessageFlow() {
        // Mock the producer to trigger the consumer directly
        doAnswer(invocation -> {
            Object message = invocation.getArgument(0);
            mqConsumer.handleMessage(message);
            return null;
        }).when(mqProducer).send(any());

        // Trigger the workflow in Project1
        project1MockMvc.perform(post("/project1/api/send-mq"))
                .andExpect(status().isOk());

        // Verify the consumer processed the message
        verify(mqConsumer).handleMessage(any());
    }
}

Final Recommendations

  • Use 方案1 if bean conflicts are minimal—it’s the closest to a real production environment.
  • Use 方案2 if bean conflicts are rampant—it avoids the headache of merging contexts entirely.
  • Always pair either approach with MQ mocking/embedded servers to cover all communication paths.

内容的提问来源于stack exchange,提问作者Vladimir Kruglov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 13:24:05