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

Andriod 单元测试(三)—— 怎么测试RxJava #179

Open
soapgu opened this issue Dec 6, 2022 · 0 comments
Open

Andriod 单元测试(三)—— 怎么测试RxJava #179

soapgu opened this issue Dec 6, 2022 · 0 comments
Labels
Demo Demo ReactiveX ReactiveX 安卓 安卓

Comments

@soapgu
Copy link
Owner

soapgu commented Dec 6, 2022

  • 前言

在接触单元测试的一开始的阶段,我就对测试异步有疑惑,异步功能确实对我的代码编写有大大的帮助,但是测试就遇到一些麻烦。因为单元测试一般都是输入输出成对匹配出现,但是异步就麻烦了,因为输出不是马上获得的。
这里StackOverflow上也有类似问题
How to use JUnit to test asynchronous processes
里面有一些思路是异步转同步的思路

   @Test
    public void testDataRetrieval() throws Exception {
        Database db = new MockDatabaseImpl();
        db.getData(DATA_LIMIT, new DataCallback() {
            @Override
            public void onSuccess(List<Data> data) {
                receiveddata = data;
                lock.countDown();
            }
        });

        lock.await(2000, TimeUnit.MILLISECONDS);

        assertNotNull(receiveddata);
        assertEquals(DATA_LIMIT, receiveddata.size());
    }

类似代码实在有点——蛋疼
这次正好借着学习单元测试的机会把异步测试这块捋一捋清楚

  • 出一个demo

先写一个RxJava的例子

HttpBin测试网站是一个专门测试http的公网站点
我们用/uuid路由获取一个随机uuid,格式为application/json

先写一个RxJava的公共方法

package com.demo.myunittest.util;

import com.google.gson.Gson;

import org.jetbrains.annotations.NotNull;

import java.io.IOException;

import io.reactivex.rxjava3.core.Single;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class HttpClientWrapper {
    private static volatile HttpClientWrapper instance;
    private  OkHttpClient client;


    private HttpClientWrapper()
    {
        client = new OkHttpClient();
    }

    public static HttpClientWrapper getInstance()
    {
        if( instance == null ) {
            synchronized (HttpClientWrapper.class) {
                if( instance == null ){
                    instance = new HttpClientWrapper();
                }
            }
        }
        return instance;
    }

    public <T> Single<T> ResponseJson(Request request , Class<T> classOfT) {
        Call call = client.newCall(request);
        return Single.create(
                emitter -> {
                    call.enqueue(new Callback() {
                        @Override
                        public void onFailure(@NotNull Call call, @NotNull IOException e) {
                            emitter.onError(e);
                        }

                        @Override
                        public void onResponse(@NotNull Call call, @NotNull Response response) {
                            try (ResponseBody body = response.body()) {
                                if (response.isSuccessful()) {
                                    try {
                                        assert body != null;
                                        String json = body.string();
                                        Gson gson = new Gson();
                                        T jsonObj = gson.fromJson(json, classOfT);
                                        emitter.onSuccess(jsonObj);
                                    } catch (Exception e) {
                                        emitter.onError(e);
                                    }
                                } else {
                                    emitter.onError(new Exception(String.format("error state code: %s", response.code())));
                                }
                            } catch (Exception e) {
                                emitter.onError(e);
                            }
                        }
                    });
                    emitter.setCancellable(call::cancel);
                }
        );

    }
}

这里基本延用了Android 练手之旅——RxAndroid&OKHttpClient(一)之中的代码。
注意:这里有一点点改进,增加了Cancel逻辑的处理
emitter.setCancellable(call::cancel);
这句代码保证了订阅被取消的时候会调用Call的cancel方法取消http的发送,做到节省资源的目的

接下来是MainActivity的代码

public class MainActivity extends AppCompatActivity {

    private final CompositeDisposable disposables = new CompositeDisposable();
    TextView tv_message;
    private static final String url = "https://httpbin.org/uuid";


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.tv_message = findViewById(R.id.tv_message);
        findViewById(R.id.button_test).setOnClickListener( v -> requestUuid() );
    }

    public void requestUuid() {
        Request request = new Request.Builder()
                .url(url)
                .get()
                .build();
        disposables.add(
            HttpClientWrapper.getInstance().ResponseJson(request, UuidResponse.class)
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe( uuid -> this.tv_message.setText(uuid.getUuid())
                            ,throwable -> this.tv_message.setText(throwable.getMessage()))
        );
    }
}

点击按钮显示最新的uuid。在模拟器测试下没啥问题。好了,我的目标就是把ResponseJson方法写进单元测试。

  • 在Unit Test中加入RxJava

增加一个RxJavaTest.java的文件

  1. 先加入一个RxJava的hello world
    我有三个选择

1)使用toBlocking()成员方法,把Rx同步化。这也是邓兄以前使用过的方案,应该可以用,但是主要缺点是会卡死进程直到整个Rx完成。

2)使用TestSubscriber来代替常规的订阅
这应该是一个比较好的方法。
因为TestSubscriber自带很多好用的assertXXXX方法
比如

       TestSubscriber<String> ts = new TestSubscriber<>();
        ts.awaitDone(5, TimeUnit.SECONDS);
        ts.assertNoErrors();
        ts.assertValue("hello");
可以从github官方的的respository里面的测试代码参考,可以当“标准答案”
 比如[SingleTest](https://github.com/ReactiveX/RxJava/blob/3.x/src/test/java/io/reactivex/rxjava3/single/SingleTest.java)
  1. 使用test()成员方法产生 TestObserver 类型对象来测试
    TestObserver和TestSubscriber很多方法类似,只要能测试就行,那我就先试试用TestObserver测试
@Test
  public void helloWorld(){
      Single.timer(1, TimeUnit.SECONDS)
              .test()
              .awaitCount(1)
              .assertValue(0L);
  }

好的,第一个RxJava的测试用例完成了。
只会有一个值,所以用了.awaitCount(1),因为值就是long的0,所以使用.assertValue(0L)
ide里面运行下
图片

测试通过,用时1s39ms,正好1秒多一点,主要的时间都拿来“等”timer了

  1. TestScheduler的使用
    前面的helloworld也看到了,测试没问题,就是要“白白”的等。
    如果是短信验证码“冷却”,要等的60秒,那不是整个测试方法要等1分钟!
    这里RxJava帮我们想过了,有一个TestScheduler帮我们达成“时光旅行”
    图片
    这里应该还是针对哪些针对时钟触发的Observer来说的。直接上代码
    @Test
    public void testByScheduler() {
        final TestScheduler testScheduler = new TestScheduler();
        TestObserver<Long> testObserver = Observable
                .intervalRange(1,5,0,5,TimeUnit.SECONDS,testScheduler)
                .test();
        testObserver.assertNoValues();
        testScheduler.advanceTimeBy(20,TimeUnit.SECONDS);
        testObserver.assertResult(1L,2L,3L,4L,5L);
    }

这里首先把TestScheduler创建出来,再传入到intervalRange里面去

图片

这里的例子是这个Rx是一个一共持续20秒,每过5秒吐一个值出来,一个出1,2,3,4,5个值 刚刚一开始是没有值的,所以是assertNoValues 接下来用testScheduler调用advanceTimeBy,把时间“前进”20秒,看看是不是获取1,2,3,4,5这些值了

图片

运行结果正常,而且时间才用了23ms。效率还高了

接下来再做一个边界测试,如果只前进19秒会如何,应该是没有5才对!

改成testScheduler.advanceTimeBy(19,TimeUnit.SECONDS);运行
图片
符合预期!完美

  1. 测试http
    好了上最终测试。
    首先这是“真实世界”的异步,TestScheduler用不了。
    所以只能用TestObserver 或者TestSubscriber了
   @Test
    public void testHttp() {
        String url = "https://httpbin.org/uuid";
        Request request = new Request.Builder()
                .url(url)
                .get()
                .build();
        TestObserver<UuidResponse> testObserver = HttpClientWrapper.getInstance().ResponseJson(request, UuidResponse.class)
                .test();
        testObserver.assertNoValues();
        testObserver.awaitCount(1);
        testObserver.assertComplete();
        assertEquals( 36 ,testObserver.values().get(0).getUuid().length() );
    }

这里uuid是随机的,但是长度是固定的,所以就assert字符串的length了
图片
测试通过!由于是冷连接,加上3次握手和外网地址4秒多有一点慢

  1. CI测试
    最后再测试一把Actions。
    先把错误的Test Case先注销了

图片

再触发一下流水线

图片

@soapgu soapgu added 安卓 安卓 ReactiveX ReactiveX labels Dec 6, 2022
@soapgu soapgu added the Demo Demo label Dec 14, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Demo Demo ReactiveX ReactiveX 安卓 安卓
Projects
None yet
Development

No branches or pull requests

1 participant