单元测试中Mock的s3Object.getObjectContent()首次调用后返回空的原因
问题:Mock的S3Object.getObjectContent()仅首次调用返回有效内容,后续调用返回空
测试代码
import com.amazonaws.services.s3.model.S3Object; import com.amazonaws.services.s3.model.S3ObjectInputStream; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Builder; import lombok.Data; import org.apache.http.client.methods.HttpRequestBase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.LinkedHashSet; import java.util.List; import java.util.stream.Collectors; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.WARN) public class S3GetObjectContentTest { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @Mock private HttpRequestBase httpRequestBase; @Mock private S3Object s3Object; private final LinkedHashSet<Pet> s3FileContents = new LinkedHashSet<>(); @BeforeEach void setup() throws IOException { s3FileContents.clear(); s3FileContents.addAll( List.of( Pet.builder().name("Jerry").type("mouse").build(), Pet.builder().name("Tom").type("cat").build(), Pet.builder().name("Roger").type("bunny").build())); Mockito.when(s3Object.getObjectContent()) .thenReturn( new S3ObjectInputStream( new ByteArrayInputStream(OBJECT_MAPPER.writeValueAsBytes(s3FileContents)), httpRequestBase)); } @Test public void doTest() { for(int n=0; n < 3; n++){ try (final InputStreamReader streamReader = new InputStreamReader(s3Object.getObjectContent(), StandardCharsets.UTF_8); final BufferedReader reader = new BufferedReader(streamReader)) { String text = reader.lines().collect(Collectors.joining()); System.out.println("reader-content: "+ (text.isEmpty() ? "\"\"" : text)); } catch (IOException e) { throw new RuntimeException(e); } } } @Data @Builder public static class Pet { public final String name; public final String type; } }
预期输出
reader-content: [{"name":"Jerry","type":"mouse"},{"name":"Tom","type":"cat"},{"name":"Roger","type":"bunny"}] reader-content: [{"name":"Jerry","type":"mouse"},{"name":"Tom","type":"cat"},{"name":"Roger","type":"bunny"}] reader-content: [{"name":"Jerry","type":"mouse"},{"name":"Tom","type":"cat"},{"name":"Roger","type":"bunny"}]
实际输出
reader-content: [{"name":"Jerry","type":"mouse"},{"name":"Tom","type":"cat"},{"name":"Roger","type":"bunny"}] reader-content: "" reader-content: ""
问题核心
为什么Mock的s3Object.getObjectContent()仅在首次调用时返回有效内容,后续调用返回空?
原因分析
问题出在Mockito的thenReturn行为和InputStream的特性上:
Mockito.when(s3Object.getObjectContent()).thenReturn(...)只会创建一次S3ObjectInputStream实例,后续所有调用getObjectContent()都会返回同一个对象。- InputStream是一次性读取的流:首次读取时,流的指针会移动到末尾,后续再读取这个流就会返回空内容。
- 代码中每次循环都会读取同一个流实例,第一次读完后流已被耗尽,所以后续两次读取自然为空。
你提到把Mock逻辑放到循环里能解决问题,是因为每次循环都会重新调用thenReturn,创建全新的S3ObjectInputStream实例,每次读取的都是未被使用过的新流。但这种方式会让测试代码冗余,不符合简洁性要求。
正确解决方案
使用Mockito的thenAnswer方法,让每次调用getObjectContent()时都返回全新的S3ObjectInputStream实例,替换setup方法中的Mock逻辑即可:
@BeforeEach void setup() throws IOException { s3FileContents.clear(); s3FileContents.addAll( List.of( Pet.builder().name("Jerry").type("mouse").build(), Pet.builder().name("Tom").type("cat").build(), Pet.builder().name("Roger").type("bunny").build())); // 替换thenReturn为thenAnswer,每次调用都生成新的流 Mockito.when(s3Object.getObjectContent()) .thenAnswer(invocation -> new S3ObjectInputStream( new ByteArrayInputStream(OBJECT_MAPPER.writeValueAsBytes(s3FileContents)), httpRequestBase)); }
这样每次调用getObjectContent()都会创建新的ByteArrayInputStream和S3ObjectInputStream,保证每次读取的都是未被耗尽的流,循环三次都会输出预期内容。
内容的提问来源于stack exchange,提问作者SourceVisor
相关产品推荐
相关产品推荐

