搭建第一个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. 完整的依赖与初始化关系图
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());
}