多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:
- Add all 6 projects as dependencies to your test project (either as modules or JARs).
- 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 } }
- 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:
- Add WireMock to your test dependencies:
<dependency> <groupId>com.github.tomakehurst</groupId> <artifactId>wiremock-jre8</artifactId> <scope>test</scope> </dependency>
- 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
@EmbeddedKafkato 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
@RabbitTestor an embedded RabbitMQ server. - Simple Mocking: Use
@MockBeanto 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

