App开发:模拟服务器数据接口 - MockApi

为了方便app开发过程当中,不受服务器接口的限制,便于客户端功能的快速测试,能够在客户端实现一个模拟服务器数据接口的MockApi模块。本篇文章就尝试为使用gradle的android项目设计实现MockApi。java

需求概述

在app开发过程当中,在和服务器人员协做时,通常会第一时间肯定数据接口的请求参数和返回数据格式,而后服务器人员会尽快提供给客户端可调试的假数据接口。不过有时候就算是假数据接口也来不及提供,或者是接口数据格式来回变更——极可能是客户端展现的缘由,这个是产品设计决定的,总之带来的问题就算服务器端的开发进度会影响客户端。android

因此,若是能够在客户端的正常项目代码中,天然地(不影响最终apk)添加一种模拟服务器数据返回的功能,这样就能够很方便的在不依赖服务器的状况下展开客户端的开发。并且考虑一种状况,为了测试不一样网络速度,网络异常以及服务器错误等各类“可能的真实数据请求的场景”对客户端UI交互的影响,咱们每每须要作不少手动测试——千篇一概!若是本地有一种控制这种服务器响应行为的能力那真是太好了。git

本文将介绍一种为客户端项目增长模拟数据接口功能的方式,但愿能减小一些开发中的烦恼。github

设计过程

下面从分层设计、可开关模拟模块、不一样网络请求结果的制造这几个方面来阐述下模拟接口模块的设计。
为了表达方便,这里要实现的功能表示为“数据接口模拟模块”,对应英文为MockDataApi,或简写为MockApi,正常的数据接口模块定义为DataApi。算法

分层思想

说到分层设计,MVC、MVP等模式必定程度上就起到了对代码所属功能的一个划分。分层设计简单的目标就是让项目代码更加清晰,各层相互独立,好处很少说。json

移动app的逻辑主要就是交互逻辑,而后须要和服务器沟通数据。因此最简单的情形下能够将一个功能(好比一个列表界面)的实现分UI层和数据访问层。api

下面将数据访问层表述为DataApi模块,DataApi层会定义一系列的接口来描述不一样类别的数据访问请求。UI层使用这些接口来获取数据,而具体的数据访问实现类就能够在不修改UI层代码的状况下进行替换。服务器

例如,有一个ITaskApi定义了方法List<Task> getTasks(),UI层一个界面展现任务列表,那么它使用ITaskApi来获取数据,而具体ITaskApi的实现类能够由DataApi层的一个工厂类DataApiManager来统一提供。网络

有了上面的分层设计,就能够为UI层动态提供真实数据接口或模拟数据接口。app

模拟接口的开关

可能你们都经历过在UI层代码里临时写一些假数据得状况。好比任务列表界面,开发初,能够写一个mockTaskData()方法来返回一个List<Task>。但这种代码只能是开发阶段有,最终apk不该该存在。

不能让“模拟数据”的代码处处散乱,在分层设计的方式下,能够将真实的数据接口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模块的实现。

UI层代码

做为示例,界面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<List<Task>>() {
                @Override
                public void onSuccess(List<Task> 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()得到数据访问接口的实例。
考虑到数据请求会是耗时的异步操做,这里每一个数据接口方法接收一个DataApiCallback<T> 回调对象,T是将返回的数据类型。

public interface DataApiCallback<T>  {

    void onSuccess(T data);

    void onError(Throwable e);

    void onStart();
}

接口DataApiCallback定义了数据接口请求数据开始和结束时的通知。

DataApiManager

根据分层设计,UI层和数据访问层之间的通讯就是基于DataApi接口的,每一个DataApi接口提供一组相关数据的获取方法。获取Task数据的接口就是ITaskApi:

public interface ITaskApi {
    void getTasks(DataApiCallback<List<Task>> 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<List<Task>> callback) {
        // 暂时没用实际的数据接口实现
        callback.onError(new Exception("数据接口未实现"));
    }
}

MockApiManager

DataApiManager利用MockApiManager来获取数据接口的mock实例。这样的好处是模拟数据接口的相关类型都被“封闭”起来,仅经过一个惟一类型来获取已知的DataApi的一种(这里就指mock)实例。这样为分离出mock相关代码打下了基础。

在DataApiManager中,获取数据接口实例时会根据开关变量MOCK_ENABLE判断是否能够返回mock实例。仅从功能上看是知足动态提供MockApi的要求了。不过,为了让最终release构建的apk中不包含多余的mock相关的代码,能够利用gradle提供的buildVariant。

  • buildVariant
    使用gradle来构建项目时,能够指定不一样的buildType,默认会有debug和release两个“构建类型”。此外,还能够提供productFlavors来提供不一样的“产品类型”,如demo版,专业版等。
    每一种productFlavor和一个buildType组成一个buildVariant(构建变种)。
    能够为每个buildType,buildVariant,或productFlavor指定特定的代码资源。

这里利用buildType来为debug和release构建分别指定不一样的MockApiManager类的实现。

默认的项目代码是在src/main/java/目录下,建立目录/src/debug/java/来放置只在debug构建时编译的代码。在/src/release/java/目录下放置只在release构建时编译的代码。

  • debug构建时的MockApiManager
public class MockApiManager {
    private static final MockApiManager INSTANCE = new MockApiManager();
    private HashMap<String, BaseMockApi> mockApis;

    private MockApiManager() {}

    public static <T> T getMockApi(Class<T> 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的那些接口的实现类对象。

  • release构建时的MockApiManager
public class MockApiManager {

    public static <T> T getMockApi(Class<T> dataApiClass) {
        return null;
    }   
}

由于最终release构建时是不须要任何mock接口的,因此此时getMockApi()一概返回null。也没有任何和提供mock接口相关的类型。

经过为debug和release构建提供不一样的MockApiManager代码,就完全实现了MockApi代码的动态添加和移除。

MockApi的实现

模拟数据接口的思路很是简单:根据请求的次数callCount,运行必定的策略来不断地返回不一样的响应结果。
响应结果包括“网络错误、服务器错误、成功”三种状态,并且还提供必定的网络时间延迟的模拟。

IMockApiStrategy

接口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构建,倒也没太大意义。

BaseMockApi

针对每个数据访问接口,均可以提供一个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<Object> 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<Object>() {
                    @Override
                    public void call(Object o) {
                        callback.onError(new IOException("mock network error."));
                    }
                };

                break;
            case Response.STATE_SERVER_ERROR:
                onNext = new Action1<Object>() {
                    @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 <T> void giveSuccessResult(final Func0<T> dataMethod, final DataApiCallback<T> callback, final Response response) {
        AndroidSchedulers.mainThread().createWorker().schedule(new Action0() {
            @Override
            public void call() {
                Observable.create(new Observable.OnSubscribe<T>() {
                    @Override
                    public void call(Subscriber<? super T> subscriber) {
                        Log.d("MOCK", "onNext Thread = " + Thread.currentThread().getName());
                        subscriber.onNext(dataMethod.call());
                        subscriber.onCompleted();
                    }
                }).
                delay(response.delayMillis, TimeUnit.MILLISECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new ApiSubcriber(callback));
            }
        });
    }

    private static class ApiSubcriber<T> extends Subscriber<T> {
        private DataApiCallback<T> callback;

        public ApiSubcriber(DataApiCallback<T> 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);
        }
    }
}
  • onResponse()
    方法onResponse()根据“响应策略”来针对一次请求返回一个“响应结果”,默认的策略由方法getMockApiStrategy()提供,子类能够重写它提供其它策略。固然策略对象自己也能够做为参数传递(此时此方法自己也没多大意义了)。
    一个想法是,每个MockApi类都只须要一个实例,这样它的callCount就能够在程序运行期间获得保持。此外,大多数状况下策略对象只须要一个就好了——它是无状态的,封装算法的一个“函数对象”,为了多态,没办法让它是静态方法。

  • giveErrorResult()
    此方法用来执行错误回调,此时是不须要数据的,只须要根据response来执行必定的延迟,而后返回网络错误或服务器错误。
    注意必定要在main线程上执行callback的各个方法,这里算是一个约定,方便UI层直接操做一些View对象。

  • giveSuccessResult()
    此方法用来执行成功回调,此时须要提供数据,并执行response中的delayMillis延迟。
    参数dataMethod用来提供须要的假数据,这里保证它的执行在非main线程中。
    一样,callback的方法都在main线程中执行。

上面BaseMockApi中的rxjava的一些代码都很是简单,彻底可使用Thread来实现。

提供MockTaskApi

做为示例,这里为ITaskApi提供了一个mock实现类:

public class MockTaskApi extends BaseMockApi implements ITaskApi {

    @Override
    public void getTasks(DataApiCallback<List<Task>> callback) {
        Response response = onResponse();

        if (response.state == Response.STATE_SUCCESS) {
            Func0<List<Task>> mockTasks = new Func0<List<Task>>() {
                @Override
                public List<Task> call() {
                    // here to give some mock data, you can get it from a json file —— if there is.
                    ArrayList<Task> 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);
                    }

                    return tasks;
                }
            };

            giveSuccessResult(mockTasks, callback, response);
        } else {

            giveErrorResult(callback, response);
        }
    }
}

它的代码几乎不用过多解释,使用代码提供须要的返回数据是很是简单的——就像你直接在UI层的Activity中写一个方法来造假数据那样。

小结

不管如何,通过上面的一系列的努力,模拟数据接口的代码已经稍具模块性质了,它能够被动态的开关,不影响最终的release构建,能够为须要测试的数据接口灵活的提供想要的mock实现。

很值得一提的是,整个MockApi模块都是创建在纯java代码上的。这样从UI层请求到数据访问方法的执行,都最终是直接的java方法的调用,这样能够很容易获取调用传递的“请求参数”,这些参数都是java类。而若是mock是创建在网络框架之上的,那么额外的http报文的解析是必不可少的。
仅仅是为了测试的目的,分层设计,让数据访问层能够在真实接口和mock接口间切换,更简单直接些。

最后,造假数据固然也能够是直接读取json文件这样的方式来完成,若是服务器开发人员有提供这样的文件的话。

示例源码

以上所述代码能够在这里获取到:
https://github.com/everhad/AndroidMockApi

若是你的项目里有模拟服务器接口这样的须要,try it out!

(本文使用Atom编写)

相关文章
相关标签/搜索