Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Http神器组件Retrofit的使用(三)——mock 加强篇 #112

Open
3 of 6 tasks
soapgu opened this issue Feb 23, 2022 · 0 comments
Open
3 of 6 tasks

Http神器组件Retrofit的使用(三)——mock 加强篇 #112

soapgu opened this issue Feb 23, 2022 · 0 comments
Labels
Demo Demo IoC New feature or request ReactiveX ReactiveX 安卓 安卓

Comments

@soapgu
Copy link
Owner

soapgu commented Feb 23, 2022

  • 序言

接上一篇的博客 Http神器组件Retrofit的使用(二)——mock in Retrofit
的基础上对Retrofit的mock做一下完善收尾,可以对Retrofit的用法暂时收个尾。
上次mock以及解决的问题

  • 对RxJAVA的兼容性
  • 对ResponseBody二进制内容对mock
  • 是否支持IoC框架

本博客将解决以下问题,从小到大排列

  • 解决在mock ResponseBody过程中需要进行InputStream,OutputStream转换的性能问题的解决

这个点一时是我的一个痒点,因为上次我是用byte[] 做“中介”来中转啦,非常浪费内存和性能,虽然一张图片也没多大,但是还是想要写一个相对精致一点的解决方案。 我们都是讲究人嘛

  • 针对异常的精确Mock,如服务端返回404,网络异常,自定义抛出异常处理

虽然正常系mock以及完美实现了demo数据,做一个用不出错的数据“提供商”。但是正式环境谁都不能保证不出异常。针对异常mock也是对mock功能很好的补充,可能针对UT测试显得更有意义一些

  • 写一个抽象类帮助实现mock和real service的调度切换适配

这也是我的终极目的,我们博客都是我们实际中遇到的问题和假想,然后再做提炼,以这个demo程序作为“理想模型”为试验田,最终的结论还是要反哺到我们的项目中

  • 从OutputStream到InputStream

  • OutputStream?InputStream?傻傻分不清楚
    从直觉来说我OutputStream流出来,直接再到InputStream流进去不是很顺,一点难度没有嘛
    你的直觉骗了你

其实 这Output 和 Input 参照物是程序
OutputStream 方向= app -> io 动作 = write
InputStream 方向= io -> app 动作 = read

commio有流复制的api不过方向是反的!

Copies bytes from an InputStream to an OutputStream.
This method buffers the input internally, so there is no need to use a BufferedInputStream.

最后使用一个PipedOutputStream关联到PipedInputStream输出

改造如下

private InputStream convertStream( ByteArrayOutputStream source ) throws IOException {
        int length = source.size();
        PipedInputStream in = new PipedInputStream();
        PipedOutputStream out = new PipedOutputStream(in);
        new Thread(new Runnable() {
            public void run () {
                try {
                    // write the original OutputStream to the PipedOutputStream
                    // note that in order for the below method to work, you need
                    // to ensure that the data has finished writing to the
                    // ByteArrayOutputStream
                    source.writeTo(out);
                }
                catch (IOException e) {
                    // logging and exception handling should go here
                }
                finally {
                    // close the PipedOutputStream here because we're done writing data
                    // once this thread has completed its run
                    if (out != null) {
                        // close the PipedOutputStream cleanly
                        try {
                            out.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
        return in;
    }

@Override
    public Single<ResponseBody> getImageFile(String url) {
        Bitmap bitmap = BitmapFactory.decodeResource(this.context.getResources(), R.drawable.picture);
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG , 100 ,stream );
        int length = stream.size();
        InputStream in;
        try {
            in = this.convertStream(stream);
        }
        catch (Exception ex){
            return fakeException(ex);
        }
        Source source = Okio.source(in);
        BufferedSource bufferedSource = Okio.buffer(source);
        ResponseBody body = new RealResponseBody("image/jpeg",length,bufferedSource);
        return this.delegate.returningResponse(body).getImageFile(url);
    }

至少避免了中间数组的尴尬

相关参考

这让我想到一部神片
图片
电影里面的技术出神入化,把谋杀做成意外神不知鬼不觉。

在程序的时间,要“制造”指定的异常在没有合适工具的前提下也比较麻烦,难道还要拔网线,服务端“配合”你一下吗。
Retrofit的mock组件不需要电影里面这么“高”的技术含量

我们通过调用retrofit2.mock.Calls的静态方法

  • Factory methods for creating Call instances which immediately respond or fail.
  1. 制造一个“意外”,抛出指定异常
private Single<ResponseBody> getImageFileSimpleException(String url){
        return this.delegate.returning(Calls.failure( new Exception("on no!!!") )).getImageFile(url);
    }

结果

2022-02-23 15:11:10.410 9055-9094/com.soapgu.helloretrofit E/SoapAPP: ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2022-02-23 15:11:10.410 9055-9094/com.soapgu.helloretrofit E/SoapAPP: │ Thread: pool-2-thread-2
2022-02-23 15:11:10.410 9055-9094/com.soapgu.helloretrofit E/SoapAPP: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2022-02-23 15:11:10.411 9055-9094/com.soapgu.helloretrofit E/SoapAPP: │ MainViewModel$$ExternalSyntheticLambda1.accept  (null:2)
2022-02-23 15:11:10.411 9055-9094/com.soapgu.helloretrofit E/SoapAPP: │    MainViewModel.lambda$newRandomPhoto$4  (MainViewModel.java:80)
2022-02-23 15:11:10.411 9055-9094/com.soapgu.helloretrofit E/SoapAPP: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2022-02-23 15:11:10.411 9055-9094/com.soapgu.helloretrofit E/SoapAPP: │ get randomPhoto error : java.lang.Exception: on no!!!
2022-02-23 15:11:10.411 9055-9094/com.soapgu.helloretrofit E/SoapAPP: │ 	at com.soapgu.helloretrofit.restful.MockPhotoApi.getImageFileSimpleException(MockPhotoApi.java:104)
2022-02-23 15:11:10.411 9055-9094/com.soapgu.helloretrofit E/SoapAPP: │ 	at com.soapgu.helloretrofit.restful.MockPhotoApi.getImageFile(MockPhotoApi.java:55)
2022-02-23 15:11:10.411 9055-9094/com.soapgu.helloretrofit E/SoapAPP: │ 	at com.soapgu.helloretrofit.restful.PhotoApiAdapter.getImageFile(PhotoApiAdapter.java:33)
2022-02-23 15:11:10.411 9055-9094/com.soapgu.helloretrofit E/SoapAPP: │ 	at com.soapgu.helloretrofit.viewmodels.MainViewModel.lambda$newRandomPhoto$0$com-soapgu-helloretrofit-viewmodels-MainViewModel(MainViewModel.java:65)
  1. 再试试弄个404
private Single<ResponseBody> getImageFile404(String url){
        Response<ResponseBody> response = Response.error(404,ResponseBody.create(MediaType.parse("application/json"),""));
        return this.delegate.returning(Calls.response(response)).getImageFile(url);
    }

输出

2022-02-23 15:15:08.671 9195-9226/com.soapgu.helloretrofit E/SoapAPP: ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2022-02-23 15:15:08.672 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ Thread: pool-2-thread-2
2022-02-23 15:15:08.672 9195-9226/com.soapgu.helloretrofit E/SoapAPP: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2022-02-23 15:15:08.672 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ MainViewModel$$ExternalSyntheticLambda1.accept  (null:2)
2022-02-23 15:15:08.672 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │    MainViewModel.lambda$newRandomPhoto$4  (MainViewModel.java:80)
2022-02-23 15:15:08.672 9195-9226/com.soapgu.helloretrofit E/SoapAPP: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2022-02-23 15:15:08.673 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ get randomPhoto error : retrofit2.adapter.rxjava3.HttpException: HTTP 404 Response.error()
2022-02-23 15:15:08.673 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ 	at retrofit2.adapter.rxjava3.BodyObservable$BodyObserver.onNext(BodyObservable.java:57)
2022-02-23 15:15:08.673 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ 	at retrofit2.adapter.rxjava3.BodyObservable$BodyObserver.onNext(BodyObservable.java:38)
2022-02-23 15:15:08.673 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ 	at retrofit2.adapter.rxjava3.CallEnqueueObservable$CallCallback.onResponse(CallEnqueueObservable.java:62)
2022-02-23 15:15:08.673 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ 	at retrofit2.mock.BehaviorCall$1$1.onResponse(BehaviorCall.java:111)
2022-02-23 15:15:08.673 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ 	at retrofit2.mock.Calls$FakeCall.enqueue(Calls.java:113)
2022-02-23 15:15:08.673 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ 	at retrofit2.mock.BehaviorCall$1.run(BehaviorCall.java:106)
2022-02-23 15:15:08.673 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ 	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:462)
2022-02-23 15:15:08.673 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ 	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
2022-02-23 15:15:08.673 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ 	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
2022-02-23 15:15:08.673 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ 	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
2022-02-23 15:15:08.673 9195-9226/com.soapgu.helloretrofit E/SoapAPP: │ 	at java.lang.Thread.run(Thread.java:923)
2022-02-23 15:15:08.673 9195-9226/com.soapgu.helloretrofit E/SoapAPP: └────────────────────────────────────────────────────────────────────────────────────────────────────────────────

嘿,这异常输出以假乱真,和“真的”404没啥区别

  • 参考

  • Retrofit 2 – Mocking HTTP Responses

  • 实现一个mock可配置化的抽象类

  • 架构图
    图片
    从架构上来讲,我们基本上是实现填色区域内

  • 目前Retrofit实现的UML图
    图片
    已经可以对http的真实数据有个很好的交互

  1. apiClass: 定义具体的http协议层
  2. ApiAdapter:我们定义的抽象层,作为具体ApiAdapter的基类
  3. PhotoApiAdapter:具体业务层的适配类,用来提供给ViewModel调用
  • 改造后的UML

图片

设计思路延续上一版,提取了一个ApiAccess接口和一个关键的MockApiAdapter抽象层,设计目的是这个类可以实现mock和真实数据的自由切换

  1. apiClasss: 定义具体的http协议层保持不变
  2. ApiAdapter
public abstract class ApiAdapter<T> implements ApiAccess<T> {
    private final T api;

    public T getApi(){
        return api;
    }

    public ApiAdapter(Retrofit retrofit , Class<T> classOfT ){
        this.api = retrofit.create(classOfT);
    }
}

保持不变,增加了ApiAccess的implements 实现约束
这里api获取的参数单一造成了api可以再构造里面一次成型,所以标记了final

  1. ApiAccess
public interface ApiAccess<T> {
    T getApi();
}

api协议层抽象获取接口

  1. MockApiAdapter
    最承上启下的核心抽象类来了
public abstract class MockApiAdapter  <T,V extends T> extends ApiAdapter<T> implements ApiAccess<T> {
    private final BehaviorDelegate<T> delegate;
    private T api;
    private final boolean mock;

    public MockApiAdapter(Retrofit retrofit, MockRetrofit mockRetrofit, Class<T> classOfT , boolean mock){
        super(retrofit,classOfT);
        this.delegate = mockRetrofit.create(classOfT);
        this.mock = mock;
    }


    @Override
    public T getApi(){
        if( !mock ){
            return super.getApi();
        }
        if( api == null ){
            synchronized ( this ){
                if( api == null ) {
                    this.api = createMockService(this.delegate);
                }
            }
        }
        return api;
    }

    protected abstract V createMockService( BehaviorDelegate<T> delegate );
  • T:代表就是apiClasss类参数

  • V:代表mock apiClasss的类,当然本身也继承T

  • 构造函数:其他都介绍过了,mock是新增的是否mock数据的开关标志位,由派生类决定,构造函数生成下BehaviorDelegate

  • createMockService:这是一个抽象方法,因为MockService和真实的Service单一构造性不同,mock可能还需要一些其他的依赖,所以我这里就不限制,让派生类类来provide,这样可以最大灵活性

  • getApi:这个函数我特意Override了,因为是在mock开关关的时候是执行base.getApi的,但是mock开关打开的时候是调用createMockService实现的,这里特殊点,api不能再构造函数内完成,本类并不能实现api的独立构造。所以api的获取只能采取layz load的模式,加了双锁避免重复生成

  • 双判断+lock+lazy load参考

  1. PhotoApiAdapter
    接下来介绍下,具体适配实现类
public class PhotoApiAdapter extends MockApiAdapter<PhotoApi,MockPhotoApi> {

    private final Application application;
    private static final String client_id = "ki5iNzD7hebsr-d8qUlEJIhG5wxGwikU71nsqj8PcMM";


    public PhotoApiAdapter(Retrofit retrofit , MockRetrofit mockRetrofit , Application application , boolean mock){
        super(retrofit,mockRetrofit,PhotoApi.class,mock);
        this.application = application;
    }

    public Single<Photo> getRandomPhoto() {
        return this.getApi().getRandomPhoto( client_id );
    }

    public Single<Pair<ResponseBody,String>> getImageFile( String url , String photoId ){
        return Single.zip( this.getApi().getImageFile(url), Single.just(photoId) , (Pair::create) ) ;
    }

    @Override
    protected MockPhotoApi createMockService(BehaviorDelegate<PhotoApi> delegate) {
        return new MockPhotoApi(delegate,application);
    }
}
  • 构造参数:同样增加了mockRetrofit ,mock。application 是创建mockApi需要的参数也在构造中传入
  • createMockService:实现了抽象函数

其他的写法上没有任何变化,这样代码重构可以省力很多,ViewModel更不需要动了

  1. IoC配置实现
@Module
@InstallIn(SingletonComponent.class)
public class MyModule {
    private boolean mock = true;

    @Singleton
    @Provides
    public Retrofit provideRetrofit(){
        return new Retrofit.Builder()
                .baseUrl("https://api.unsplash.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
                .build();
    }

    @Singleton
    @Provides
    public MockRetrofit provideMockRetrofit(Retrofit retrofit){
        NetworkBehavior behavior = NetworkBehavior.create();
        return new MockRetrofit.Builder(retrofit)
                .networkBehavior(behavior)
                .build();
    }

    @Provides
    public PhotoApiAdapter providePhotoApiAdapter(Retrofit retrofit, MockRetrofit mockRetrofit, Application application){
        return new PhotoApiAdapter( retrofit, mockRetrofit, application, mock );
    }
}

没啥亮点,还算干净整洁吧,其中mock变量以后可以用配置文件的参数来替代

@soapgu soapgu added Demo Demo IoC New feature or request 安卓 安卓 ReactiveX ReactiveX labels Feb 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Demo Demo IoC New feature or request ReactiveX ReactiveX 安卓 安卓
Projects
None yet
Development

No branches or pull requests

1 participant