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

集成测试中如何向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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 14:03:16