为了方便 app 开发过程中,不受服务器接口的限制,便于客户端功能的快速测试,可以在客户端实现一个模拟服务器数据接口的 MockApi 模块。本篇文章就尝试为使用 gradle 的 android 项目设计实现一个 MockAPi。
在 app 开发过程中,在和服务器人员协作时,一般会第一时间
,然后服务器人员会尽快提供给客户端可调试的假数据接口。不过有时候就算是假数据接口也来不及提供,或者是接口数据格式来回变动——很可能是客户端展示的原因,这个是产品设计决定的,总之带来的问题就算服务器端的开发进度会影响客户端。
- 确定数据接口的请求参数和返回数据格式
所以,如果可以在客户端的正常项目代码中,自然地(不影响最终 apk)添加一种模拟服务器数据返回的功能,这样就可以很方便的在不依赖服务器的情况下展开客户端的开发。而且考虑一种情况,为了测试不同网络速度,网络异常以及服务器错误等各种 "可能的真实数据请求的场景" 对客户端 UI 交互的影响,我们往往需要做很多手动测试——千篇一律!如果本地有一种控制这种服务器响应行为的能力那真是太好了。
本文将介绍一种为客户端项目增加
功能的方式,希望能减少一些开发中的烦恼。
- 模拟数据接口
下面从
这几个方面来阐述下模拟接口模块的设计。 为了表达方便,这里要实现的功能表示为 "数据接口模拟模块",对应英文为 MockDataApi,或简写为 MockApi,正常的数据接口模块定义为 DataApi。
- 分层设计、可开关模拟模块、不同网络请求结果的制造
说到分层设计,MVC、MVP 等模式一定程度上就起到了对代码所属功能的一个划分。分层设计简单的目标就是让项目代码更加清晰,各层相互独立,好处不多说。
移动 app 的逻辑主要就是交互逻辑,然后需要和服务器沟通数据。所以最简单的情形下可以将一个功能(比如一个列表界面)的实现分 UI 层和数据访问层。
下面将数据访问层表述为 DataApi 模块,DataApi 层会定义一系列的接口来描述不同类别的数据访问请求。UI 层使用这些接口来获取数据,而具体的数据访问实现类就可以在不修改 UI 层代码的情况下进行替换。
例如,有一个 ITaskApi 定义了方法 List
有了上面的分层设计,就可以为 UI 层动态提供真实数据接口或模拟数据接口。
可能大家都经历过在 UI 层代码里临时写一些假数据得情况。比如任务列表界面,开发初,可以写一个 mockTaskData() 方法来返回一个 List
不能让 "模拟数据" 的代码到处散乱,在分层设计的方式下,可以将真实的数据接口 DataApi 和模拟数据接口 MockDataApi 分别作为两个数据接口的实现模块,这样就可以根据项目的构建类型来动态提供不同的数据接口实现。
实现 MockDataApi 的动态提供的方法也不止一种。
一般的 java 项目可以使用 "工厂模式 + 反射" 来动态提供不同的接口实现类,再专业点就是依赖注入——DI 框架的使用了。
目前 gradle 是 java 的最先进的构建工具,它支持根据 buildType 来分别指定不同的代码资源,或不同的依赖。
可以在一个单独的类库 module(就是 maven 中的项目)中来编写各种 MockDataApi 的实现类,然后主 app module 在 debug 构建时添加对它的依赖,此时数据接口的提供者 DataApiManager 可以向 UI 层返回这些 mock 类型的实例。
为了让 "正常逻辑代码" 和 mock 相关代码的关联尽量少,可以提供一个 MockApiManager 来唯一获取各个 MockDataApi 的实例。然后在 debug 构建下的 MockApiManager 会返回提供了 mock 实现的数据接口实例,而 release 构建时 MockApiManager 会一律返 null。
MockApi 在多次请求时提供不同的网络请求结果,如服务器错误,网络错误,成功等,并模拟出一定的网络延迟,这样就很好的满足了 UI 层代码的各种测试需求。
为了达到上述目标,定义一个接口 IMockApiStrategy 来表示对数据请求的响应策略,它定义了方法 onResponse(int callCount)。根据当前请求的次数 callCount,onResponse() 会得到不同的模拟响应结果。很明显,可以根据测试需要提供不同的请求响应策略,比如不断返回成功请求,或者不断返回错误请求,或轮流返回成功和错误等。
下面就给出各个部分的关键代码,来说明以上所描述的 MockDataApi 模块的实现。
作为示例,界面 MainActivity 是一个 "任务列表" 的展示。任务由 Task 类表示:
- public class Task {
- public String name;
- }
界面 MainActivity 使用一个 TextView 来显示 "加载中、任务列表、网络错误" 等效果,并提供一个 Button 来点击刷新数据。代码如下:
- public class MainActivity extends Activity {
- private TextView tv_data;
- private boolean requesting = false;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- tv_data = (TextView) findViewById(R.id.tv_data);
- getData();
- }
- private void getData() {
- if (requesting) return;
- requesting = true;
- ITaskApi api = DataApiManager.ofTask();
- if (api != null) {
- api.getTasks(new DataApiCallback>() {
- @Override
- public void onSuccess(List data) {
- // 显示数据
- StringBuilder sb = new StringBuilder("请求数据成功:\n");
- for (int i = 0; i < data.size(); i++) {
- sb.append(data.get(i).name).append("\n");
- }
- tv_data.setText(sb.toString());
- requesting = false;
- }
- @Override
- public void onError(Throwable e) {
- // 显示错误
- tv_data.setText("错误:\n" + e.getMessage());
- requesting = false;
- }
- @Override
- public void onStart() {
- // 显示loading
- tv_data.setText("正在加载...");
- }
- });
- }
- }
- public void onRefreshClick(View view) {
- getData();
- }
- }
在 UI 层代码中,使用 DataApiManager.ofTask() 获得数据访问接口的实例。
考虑到数据请求会是耗时的异步操作,这里每个数据接口方法接收一个
回调对象,T 是将返回的数据类型。
- DataApiCallback<T>
- public interface DataApiCallback {
- void onSuccess(T data);
- void onError(Throwable e);
- void onStart();
- }
接口 DataApiCallback 定义了数据接口请求数据开始和结束时的通知。
根据分层设计,UI 层和数据访问层之间的通信就是基于 DataApi 接口的,每个 DataApi 接口提供一组相关数据的获取方法。获取 Task 数据的接口就是 ITaskApi:
- public interface ITaskApi {
- void getTasks(DataApiCallback> callback);
- }
UI 层通过 DataApiManager 来获得各个 DataApi 接口的实例。也就是在这里,会根据当前项目构建是 debug 还是 release 来选择性提供 MockApi 或最终的 DataApi。
- public class DataApiManager {
- private static final boolean MOCK_ENABLE = BuildConfig.DEBUG;
- public static ITaskApi ofTask() {
- if (MOCK_ENABLE) {
- ITaskApi api = MockApiManager.getMockApi(ITaskApi.class);
- if (api != null) return api;
- }
- return new NetTaskApi();
- }
- }
当 MOCK_ENABLE 为 true 时,会去 MockApiManager 检索一个所需接口的 mock 实例,如果没找到,会返回真实的数据接口的实现,上面的 NetTaskApi 就是。倘若现在服务器还无法进行联合调试,它的实现就简单的返回一个服务器错误:
- public class NetTaskApi implements ITaskApi {
- @Override
- public void getTasks(DataApiCallback> callback) {
- // 暂时没用实际的数据接口实现
- callback.onError(new Exception("数据接口未实现"));
- }
- }
DataApiManager 利用 MockApiManager 来获取数据接口的 mock 实例。这样的好处是模拟数据接口的相关类型都被 "封闭" 起来,仅通过一个唯一类型来获取已知的 DataApi 的一种(这里就指 mock)实例。这样为分离出 mock 相关代码打下了基础。
在 DataApiManager 中,获取数据接口实例时会根据开关变量 MOCK_ENABLE 判断是否可以返回 mock 实例。仅从功能上看是满足动态提供 MockApi 的要求了。不过,为了让最终 release 构建的 apk 中不包含多余的 mock 相关的代码,可以利用 gradle 提供的 buildVariant。
这里利用 buildType 来为 debug 和 release 构建分别指定不同的 MockApiManager 类的实现。
默认的项目代码是在 src/main/java / 目录下,创建目录 / src/debug/java / 来放置只在 debug 构建时编译的代码。在 / src/release/java / 目录下放置只在 release 构建时编译的代码。
- public class MockApiManager {
- private static final MockApiManager INSTANCE = new MockApiManager();
- private HashMap mockApis;
- private MockApiManager() {}
- public static T getMockApi(Class dataApiClass) {
- if (dataApiClass == null) return null;
- String key = dataApiClass.getName();
- try {
- T mock = (T) getInstance().mockApis.get(key);
- return mock;
- } catch (Exception e) {
- return null;
- }
- }
- private void initApiTable() {
- mockApis = new HashMap<>();
- mockApis.put(ITaskApi.class.getName(), new MockTaskApi());
- }
- private static MockApiManager getInstance() {
- if (INSTANCE.mockApis == null) {
- synchronized (MockApiManager.class) {
- if (INSTANCE.mockApis == null) {
- INSTANCE.initApiTable();
- }
- }
- }
- return INSTANCE;
- }
- }
静态方法 getMockApi() 根据传递的接口类型信息从 mockApis 中获取可能的 mock 实例,mockApis 中注册了需要 mock 的那些接口的实现类对象。
- public class MockApiManager {
- public static T getMockApi(Class dataApiClass) {
- return null;
- }
- }
因为最终 release 构建时是不需要任何 mock 接口的,所以此时 getMockApi() 一律返回 null。也没有任何和提供 mock 接口相关的类型。
通过为 debug 和 release 构建提供不同的 MockApiManager 代码,就彻底实现了 MockApi 代码的动态添加和移除。
模拟数据接口的思路非常简单:根据请求的次数 callCount,运行一定的策略来不断地返回不同的响应结果。
响应结果包括 "网络错误、服务器错误、成功",而且还提供一定的时间延迟。
接口 IMockApiStrategy 的作用就是抽象对请求返回不同响应结果的策略,响应结果由 IMockApiStrategy.Response 表示。
- public interface IMockApiStrategy {
- void onResponse(int callCount, Response out);
- /**
- * Mock响应返回结果,表示响应的状态
- */
- class Response {
- public static final int STATE_NETWORK_ERROR = 1;
- public static final int STATE_SERVER_ERROR = 2;
- public static final int STATE_SUCCESS = 3;
- public int state = STATE_SUCCESS;
- public int delayMillis = 600;
- }
- }
Response 表示的响应结果包含结果状态和延迟时间。
作为一个默认的实现,WheelApiStrategy 类根据请求次数,不断返回上述的三种结果:
- public class WheelApiStrategy implements IMockApiStrategy {
- @Override
- public void onResponse(int callCount, Response out) {
- if (out == null) return;
- int step = callCount % 10;
- switch (step) {
- case 0:
- case 1:
- case 2:
- case 3:
- out.state = Response.STATE_SUCCESS;
- break;
- case 4:
- case 5:
- out.state = Response.STATE_SERVER_ERROR;
- break;
- case 6:
- case 7:
- out.state = Response.STATE_SUCCESS;
- break;
- case 8:
- case 9:
- out.state = Response.STATE_NETWORK_ERROR;
- break;
- }
- out.delayMillis = 700;
- }
- }
方法 onResponse() 的参数 out 仅仅是为了避免多次创建小对象,对应 debug 构建,倒也没太大意义。
针对每一个数据访问接口,都可以提供一个 mock 实现。比如为接口 ITaskApi 提供 MockTaskApi 实现类。
为了简化代码,抽象基类 BaseMockApi 完成了大部分公共的逻辑。
- public abstract class BaseMockApi {
- protected int mCallCount;
- private IMockApiStrategy mStrategy;
- private Response mResponse = new Response();
- public Response onResponse() {
- if (mStrategy == null) {
- mStrategy = getMockApiStrategy();
- }
- if (mStrategy != null) {
- mStrategy.onResponse(mCallCount, mResponse);
- mCallCount++;
- }
- return mResponse;
- }
- protected IMockApiStrategy getMockApiStrategy() {
- return new WheelApiStrategy();
- }
- protected void giveErrorResult(final DataApiCallback callback, Response response) {
- Action1 onNext = null;
- AndroidSchedulers.mainThread().createWorker().schedule(new Action0() {
- @Override
- public void call() {
- callback.onStart();
- }
- });
- switch (response.state) {
- case Response.STATE_NETWORK_ERROR:
- onNext = new Action1() {
- @Override
- public void call(Object o) {
- callback.onError(new IOException("mock network error."));
- }
- };
- break;
- case Response.STATE_SERVER_ERROR:
- onNext = new Action1() {
- @Override
- public void call(Object o) {
- callback.onError(new IOException("mock server error."));
- }
- };
- break;
- }
- if (onNext != null) {
- Observable.just(10086)
- .delay(response.delayMillis, TimeUnit.MILLISECONDS)
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(onNext);
- }
- }
- public void giveSuccessResult(final T data, final DataApiCallback callback, final Response response) {
- AndroidSchedulers.mainThread().createWorker().schedule(new Action0() {
- @Override
- public void call() {
- Observable.just(data)
- .delay(response.delayMillis, TimeUnit.MILLISECONDS)
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(new ApiSubcriber(callback));
- }
- });
- }
- private static class ApiSubcriber extends Subscriber {
- private DataApiCallback callback;
- public ApiSubcriber(DataApiCallback callback) {
- this.callback = callback;
- }
- @Override
- public void onStart() {
- callback.onStart();
- }
- @Override
- public void onCompleted() {}
- @Override
- public void onError(Throwable e) {
- callback.onError(e);
- }
- @Override
- public void onNext(T data) {
- callback.onSuccess(data);
- }
- }
- }
上面 BaseMockApi 中的 rxjava 的一些代码都非常简单,完全可以使用 Thread 来实现。
作为示例,这里为 ITaskApi 提供了一个 mock 实现类:
- public class MockTaskApi extends BaseMockApi implements ITaskApi {
- @Override
- public void getTasks(DataApiCallback> callback) {
- Response response = onResponse();
- if (response.state == Response.STATE_SUCCESS) {
- // 造假数据
- ArrayList tasks = new ArrayList<>();
- int start = (mCallCount - 1) * 6;
- for (int i = start; i < start + 6; i++) {
- Task task = new Task();
- task.name = "Task - " + i;
- tasks.add(task);
- }
- giveSuccessResult(tasks, callback, response);
- } else {
- giveErrorResult(callback, response);
- }
- }
- }
它的代码几乎不用过多解释,使用代码提供需要的返回数据是非常简单的——就像你直接在 UI 层的 Activity 中写一个方法来造假数据那样。
无论如何,经过上面的一系列的努力,模拟数据接口的代码已经稍具模块性质了,它可以被动态的开关,不影响最终的 release 构建,可以为需要测试的数据接口灵活的提供想要的 mock 实现。
很值得一提的是,整个 MockApi 模块都是建立在纯 java 代码上的。这样从 UI 层请求到数据访问方法的执行,都最终是直接的 java 方法的调用,这样可以很容易获取调用传递的 "请求参数",这些参数都是 java 类。而如果 mock 是建立在网络框架之上的,那么额外的 http 报文的解析是必不可少的。
仅仅是为了测试的目的,分层设计,让数据访问层可以在真实接口和 mock 接口间切换,更简单直接些。
最后,造假数据当然也可以是直接读取 json 文件这样的方式来完成,如果服务器开发人员有提供这样的文件的话。
以上所述代码可以在这里获取到:
如果你的项目里有模拟服务器接口这样的需要,try it out!
(本文使用 Atom 编写)
来源: