集成测试中如何向Spring上下文注入Mocked S3Client
问题描述
我有一个依赖S3Client的S3StorageService类:
@RequiredArgsConstructor @Service public class S3StorageService implements StorageService<S3FileUploadResult> { private final S3Client client; // ... }
还有SongServiceImpl类:
@RequiredArgsConstructor @Service public class SongServiceImpl implements SongService { private final StorageService<?> storageService; @Transactional @Override public Song uploadSong(UploadSongDTO dto) { // ... storageService.upload(dto.file()); } }
以及SongController类:
@RequiredArgsConstructor @RequestMapping("/song") @RestController public class SongController { private final SongService songService; @PostMapping("/upload") public ResponseEntity<Song> upload(@Valid UploadSongDTO dto) { return ResponseEntity.ok(songService.uploadSong(dto)); } }
我想要为SongController#upload编写集成测试,需要将模拟的S3Client注入Spring上下文,让S3StorageService使用该模拟实例而非真实S3服务。我尝试了以下代码:
static S3MockContainer s3Mock = new S3MockContainer("latest"); @BeforeEach void setUp() { final String endpoint = s3Mock.getHttpsEndpoint(); final S3Configuration serviceConfig = S3Configuration.builder() .pathStyleAccessEnabled(true) .build(); final SdkHttpClient httpClient = UrlConnectionHttpClient.builder() .buildWithDefaults(AttributeMap.builder() .put(TRUST_ALL_CERTIFICATES, Boolean.TRUE) .build()); final S3Client s3Client = S3Client.builder() // 需注入到S3StorageService .region(Region.of("auto")) .endpointOverride(URI.create(endpoint)) .serviceConfiguration(serviceConfig) .httpClient(httpClient) .build(); applicationContext.registerBean(S3Client.class, () -> s3Client); // 无效,报错:`Parameter 0 of constructor in me.approximations.music.services.storage.impl.S3StorageService required a bean of type 'software.amazon.awssdk.services.s3.S3Client' that could not be found.` }
请问该如何正确实现?
解决方案
你的问题出在Spring上下文初始化时机:@BeforeEach是在上下文加载完成后执行的,这时候S3StorageService早就已经尝试注入S3Client了,所以会报找不到Bean的错误。下面是几种可行的解决方法:
方法1:使用@MockBean快速替换(适合简单模拟场景)
如果不需要真的启动S3Mock容器,只是想模拟S3Client的行为,可以直接用Spring Boot的@MockBean注解,它会自动替换上下文中原有的S3Client Bean:
@SpringBootTest @AutoConfigureMockMvc public class SongControllerTest { @Autowired private MockMvc mockMvc; @MockBean private S3Client s3Client; // 测试方法中可以自定义s3Client的行为 @Test void uploadSongTest() throws Exception { // 模拟upload方法的返回值或行为 when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) .thenReturn(PutObjectResponse.builder().build()); // 执行MockMvc请求测试 mockMvc.perform(multipart("/song/upload") .file("file", "test-content".getBytes()) .param("name", "test-song")) .andExpect(status().isOk()); } }
方法2:用@TestConfiguration提前注册S3Client(适配S3Mock容器场景)
如果必须使用S3MockContainer提供的真实模拟服务,需要在Spring上下文初始化之前就注册好S3Client Bean。可以用@TestConfiguration来实现:
@SpringBootTest @AutoConfigureMockMvc public class SongControllerTest { static S3MockContainer s3Mock = new S3MockContainer("latest"); // 静态代码块启动S3Mock容器,确保在Spring上下文初始化前完成 static { s3Mock.start(); } // 测试专用配置类,提前注册S3Client @TestConfiguration static class TestS3Config { @Bean public S3Client s3Client() { final String endpoint = s3Mock.getHttpsEndpoint(); final S3Configuration serviceConfig = S3Configuration.builder() .pathStyleAccessEnabled(true) .build(); final SdkHttpClient httpClient = UrlConnectionHttpClient.builder() .buildWithDefaults(AttributeMap.builder() .put(TRUST_ALL_CERTIFICATES, Boolean.TRUE) .build()); return S3Client.builder() .region(Region.of("auto")) .endpointOverride(URI.create(endpoint)) .serviceConfiguration(serviceConfig) .httpClient(httpClient) .build(); } } @Autowired private MockMvc mockMvc; @Autowired private S3Client s3Client; @BeforeEach void setUp() { // 提前创建测试桶 s3Client.createBucket(CreateBucketRequest.builder().bucket("test-bucket").build()); } @Test void uploadSongTest() throws Exception { // 执行上传测试 mockMvc.perform(multipart("/song/upload") .file("file", "test-audio-content".getBytes()) .param("name", "test-song")) .andExpect(status().isOk()); } @AfterAll static void tearDown() { s3Mock.stop(); } }
注意点:
S3MockContainer要放在静态代码块中启动,确保Spring初始化Bean前容器已经就绪@TestConfiguration类要定义为静态内部类,这样才能被Spring测试上下文识别- 记得在
@AfterAll中停止S3Mock容器,避免资源泄漏
方法3:使用@DynamicPropertySource动态配置(更简洁的S3Mock集成)
如果使用的是testcontainers的S3Mock,可以结合@DynamicPropertySource自动注入配置,不需要手动构建S3Client:
@SpringBootTest @AutoConfigureMockMvc @Testcontainers public class SongControllerTest { @Container static S3MockContainer s3Mock = new S3MockContainer("latest"); @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.cloud.aws.s3.endpoint", s3Mock::getHttpsEndpoint); registry.add("spring.cloud.aws.s3.path-style-access", () -> true); registry.add("spring.cloud.aws.region.static", () -> "auto"); // 如果你的S3Client是通过Spring Cloud AWS自动配置的,这些属性会自动生成正确的S3Client Bean } @Autowired private MockMvc mockMvc; @Autowired private S3Client s3Client; @BeforeEach void setUp() { // 提前创建测试桶 s3Client.createBucket(CreateBucketRequest.builder().bucket("test-bucket").build()); } @Test void uploadSongTest() throws Exception { // 执行上传测试 mockMvc.perform(multipart("/song/upload") .file("file", "test-content".getBytes()) .param("name", "test-song")) .andExpect(status().isOk()); } }
这种方式适用于使用Spring Cloud AWS自动配置S3Client的场景,不需要手动构建客户端,配置更简洁。
内容的提问来源于stack exchange,提问作者Guilherme Silva
相关产品推荐
相关产品推荐

