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

单元测试中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的特性上:

  1. Mockito.when(s3Object.getObjectContent()).thenReturn(...)只会创建一次S3ObjectInputStream实例,后续所有调用getObjectContent()都会返回同一个对象。
  2. InputStream是一次性读取的流:首次读取时,流的指针会移动到末尾,后续再读取这个流就会返回空内容。
  3. 代码中每次循环都会读取同一个流实例,第一次读完后流已被耗尽,所以后续两次读取自然为空。

你提到把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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 11:50:08