搭建第一个Retrofit回放测试环境:依赖配置与初始化

好,到了真正动手的时候了。
前面几章我们聊了回放测试的理念,现在该把代码敲出来。

我个人习惯先把环境搭好再写逻辑。因为依赖错了、版本冲突了,debug起来比写代码还头疼。

这一章我们就干三件事:加依赖、配拦截器、初始化回放引擎。

1. 依赖配置——别小看版本号

先看 build.gradle 里要加哪些东西。

dependencies {
    // Retrofit 本体
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

    // OkHttp(Retrofit 底层网络库)
    implementation 'com.squareup.okhttp3:okhttp:4.12.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'

    // 回放测试核心:OkHttp MockWebServer 或自定义拦截器
    testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'

    // 测试框架
    testImplementation 'junit:junit:4.13.2'
    testImplementation 'org.robolectric:robolectric:4.10'
}

💡 我用过 2.6.0 的 Retrofit + 3.12.0 的 OkHttp,结果拦截器签名不兼容。
所以推荐保持大版本匹配。Retrofit 2.x 系列配 OkHttp 4.x 没问题。

为什么加 logging-interceptor?因为回放需要记录请求/响应数据,这个拦截器能帮我们拿到原始报文。

2. 初始化流程——核心三步走

接下来看代码怎么写。这一步决定了你的回放引擎能不能工作。
我习惯把初始化封装成一个方法,清晰好测。

public class ReplayInterceptor implements Interceptor {
    private final ReplayMode mode; // 回放模式:RECORD 或 PLAYBACK
    private final File recordingDir; // 录音存放路径

    public ReplayInterceptor(ReplayMode mode, File dir) {
        this.mode = mode;
        this.recordingDir = dir;
    }

    @NonNull
    @Override
    public Response intercept(@NonNull Chain chain) throws IOException {
        Request request = chain.request();
        String cacheKey = request.method() + ":" + request.url().encodedPath();

        if (mode == ReplayMode.RECORD) {
            // 记录模式:发送真实请求,保存响应
            Response response = chain.proceed(request);
            saveToFile(cacheKey, response.peekBody(1024 * 1024).string());
            return response;
        } else {
            // 回放模式:读取本地文件,跳过网络
            String cached = readFromFile(cacheKey);
            if (cached != null) {
                return buildMockResponse(request, cached);
            }
            // 没命中则 fallback 到真实请求
            return chain.proceed(request);
        }
    }
}

嗯,这里要注意:peekBody() 只能读一次,之后 body 就空了。
所以我先保存字符串,再重新组装 Response 对象。

3. 创建 OkHttpClient 并注入拦截器

File cacheDir = new File(context.getCacheDir(), "replay_recordings");
ReplayInterceptor replayInterceptor = new ReplayInterceptor(
        mode, // 由测试配置决定
        cacheDir
);

OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.HEADERS))
        .addInterceptor(replayInterceptor) // 回放拦截器必须放在日志之后
        .connectTimeout(30, TimeUnit.SECONDS)
        .build();

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .client(client)
        .addConverterFactory(GsonConverterFactory.create())
        .build();

ApiService api = retrofit.create(ApiService.class);

⚠️ 拦截器顺序很重要!
LoggingInterceptor 应该在 ReplayInterceptor 之前,这样日志能看到原始请求。

4. 完整的依赖与初始化关系图

Retrofit 回放测试环境初始化流程 依赖配置 (Gradle) OkHttpClient 构建 ReplayInterceptor 实例 注册拦截器链 Retrofit 实例创建 ApiService 代理对象 / 回放能力 说明:初始化顺序不可逆,回放拦截器必须在OkHttpClient.build()之前完成注册

5. 常见的坑与我的经验

问题 原因 对策
Response body 为空 peekBody() 只允许调用一次 先 clone body 再读取
拦截器不触发 addInterceptor vs addNetworkInterceptor 顺序错 回放用 addInterceptor,日志用 addNetworkInterceptor
文件读写权限异常 Android 11+ 分区存储限制 使用 context.getCacheDir() 避开权限问题

我曾经在项目里把 addInterceptor 写成了 addNetworkInterceptor,结果回放拦截器拿不到请求的原始 headers。
找了两天才发现是顺序问题。所以建议:日志拦截器在前,回放拦截器在后。

6. 快速验证:写一个单元测试

@Test
public void testReplayInterceptor_inPlaybackMode() throws IOException {
    // 初始化回放拦截器(回放模式)
    ReplayInterceptor interceptor = new ReplayInterceptor(
            ReplayMode.PLAYBACK,
            testFolder.newFolder()
    );

    // 构建一个假的请求
    Request request = new Request.Builder()
            .url("https://example.com/user/1")
            .get()
            .build();

    // 手动构造链(需要模拟 Chain 对象,略)
    Response response = interceptor.intercept(chain);

    // 断言:应该从本地获取数据,而不是发起网络
    assertNotNull(response);
    assertEquals(200, response.code());
}