测试异步方法

深切体会到,测试异步方法,是整个单元测试的难点和重点,为什么这么说呢?问题很明显,当测试方法跑完了的时候,被测的异步代码可能还在执行没跑完,这就有问题了。再者就是实现异步操作的框架比较多样。下面有这么一个AyncModel类:

  public class AyncModel {

      private Handler mUiHandler = new Handler(Looper.getMainLooper());

      public void loadAync(final Callback callback) {

          new Thread(new Runnable() {

              @Override

              public void run() {

                  try {

                      // 模拟耗时操作

                      Thread.sleep(1000);

                      final List<String> results = new ArrayList<>();

                      results.add("test String");

                      mUiHandler.post(new Runnable() {

                          @Override

                          public void run() {

                              callback.onSuccess(results);

                          }

                      });

                  } catch (final InterruptedException e) {

                      e.printStackTrace();

                      mUiHandler.post(new Runnable() {

                          @Override

                          public void run() {

                              callback.onFailure(500, e.getMessage());

                          }

                      });

                  }

              }

          }).start();

      }

      interface Callback {

          void onSuccess(List<String> results);

          void onFailure(int code, String msg);

      }

  }

  在上面的例子中,AyncModel类的loadAync()方法里面新建了一个线程来异步加载results字符串列表。如果我们按正常的方式写对应的测试:

  public class AyncModelTest extends BaseRoboTestCase {

      @Test

      public void loadAync() throws Exception {

          AyncModel model = new AyncModel();

          final List<String> result = new ArrayList<>();

          model.loadAync(new AyncModel.Callback() {

              @Override

              public void onSuccess(List<String> list) {

                  result.addAll(list);

              }

              @Override

              public void onFailure(int code, String msg) {

                  fail();

              }

          });

          assertEquals(1, result.size());

      }

  }

  你会发现上面的测试方法loadAync()永远会fail,这是因为在执行 assertEquals(1, result.size());的时候,loadAync()里面启动的线程压根还没执行完毕呢,因此,callback里面的 result.addAll(list);也没有得到执行,所以result.size()返回永远是0。


前方高能,重点来了,要解决这个问题:如何使用正确的姿势来测试异步代码。通常有两种思路,一是等异步代码执行完了再执行assert断言操作,二是将异步变成同步。接下来,具体讲讲用这两种思路怎样来测试我们的异步代码:

 等待异步代码执行完毕

  在上面的例子中,我们要做的其实就是是等待Callback里面的代码执行完毕后再执行Asset断言操作。要达到这个目的,大致有两种实现方式:

 (1)、使用Thread.sleep

  估计大家的第一反应可能和我一样,会使用这种休眠的方式来等待异步代码执行,可能是最简单的方式,这种方式需要设置sleep的时间,所以不可控,建议不适用这种方式。结合上面的例子,具体演示一下:

  public class AyncModelTest extends BaseRoboTestCase {

      @Test

      public void loadAync() throws Exception {

          AyncModel model = new AyncModel();

          final List<String> result = new ArrayList<>();

          model.loadAync(new AyncModel.Callback() {

              @Override

              public void onSuccess(List<String> list) {

                  result.addAll(list);

              }

              @Override

              public void onFailure(int code, String msg) {

                  fail();

              }

          });

          // 使用sleep方式等待异步执行

          Thread.sleep(4000);

          // 此处有坑,如果不加这行代码,就会出现Handler没有执行Runnable的问题

          ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

          assertEquals(1, result.size());

      }

  }

 (2)、使用CountDownLatch

  有一个非常好用的神器,那就是CountDownLatch。CountDownLatch是一个类,它有两对配套使用的方法,那就是countDown()和await()。await()方法会阻塞当前线程,直到countDown()被调用了一定的次数,这个次数就是在创建这个CountDownLatch对象时,传入的构造参数。结合上面的例子,具体如下:

  public class AyncModelTest extends BaseRoboTestCase {

      @Test

      public void loadAync() throws Exception {

          // 使用CountDownLatch

          final CountDownLatch latch = new CountDownLatch(1);

          AyncModel model = new AyncModel();

          final List<String> result = new ArrayList<>();

          model.loadAync(new AyncModel.Callback() {

              @Override

              public void onSuccess(List<String> list) {

                  result.addAll(list);

                  latch.countDown();

              }

              @Override

              public void onFailure(int code, String msg) {

                  fail();

                  latch.countDown();

              }

          });

          latch.await(3, TimeUnit.SECONDS);

          // 此处有坑,如果不加这行代码,就会出现Handler没有执行Runnable的问题

          ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

          assertEquals(1, result.size());

      }

  }

  使用CountDownLatch来做单元测试,有一个很大的限制,侵入性很高,那就是countDown()必须在测试代码里面写。换句话说,异步操作必需提供Callback,在Callback中执行countDown()方法。如果被测的异步方法(如上面例子的loadAync())不是通过Callback的方式来通知结果,而是通过EventBus来通知外面方法异步运行的结果,那CountDownLatch是无法解决这个异步方法的单元测试问题的。

 将异步变成同步

  将异步操作变成同步,是解决异步代码测试问题的一种比较直观的思路。这种思路往往比较复杂,根据项目的实际情况来抉择,大致的思想就是将异步操作转换到自己事先准备好的同步线程池来执行。

  (1)、通过Executor或ExecutorService方式

  如果你的代码是通过Executor或ExecutorService来做异步的,那在测试中把异步变成同步的做法,跟在测试中使用mock对象的方法是一样的,那就是使用依赖注入。在测试代码里面,将同步的Executor注入进去。创建同步的Executor对象很简单,以下就是一个同步的Executor:

  Executor immediateExecutor = new Executor() {

      @Override

      public void execute(Runnable command) {

          command.run();

      }

  };

 (2)、通过New Thread()方式

  如果你在代码里面直接通过new Thread()的方式来做异步,这种方式比较简单粗暴,估计你在coding时很爽。但是不幸的告诉你,这样的代码是没有办法变成同步的。那么要做单元测试的话,就需要换成Executor这种方式来做异步操作。还是结合上面的例子,我们来实践一下,修改之后的AyncModel类如下:

  public class AyncModel {

      private Handler mUiHandler = new Handler(Looper.getMainLooper());

      private Executor executor;

      public AyncModel(Executor executor) {

          this.executor = executor;

      }

      public void loadAync(final Callback callback) {

          if (executor == null) {

              executor = Executors.newCachedThreadPool();

          }

          executor.execute(new Runnable() {

              @Override

              public void run() {

                  final List<String> repos = new ArrayList<>();

                  repos.add("test String");

                  mUiHandler.post(new Runnable() {

                      @Override

                      public void run() {

                          callback.onSuccess(repos);

                      }

                  });

              }

          });

      }

      interface Callback {

          void onSuccess(List<String> results);

          void onFailure(int code, String msg);

      }

  }

  接着我们看一下修改之后的测试Case:

  public class AyncModelTest extends BaseRoboTestCase {

      @Test

      public void loadAync() throws Exception {

          // Executor

          Executor immediateExecutor = new Executor() {

              @Override

              public void execute(Runnable command) {

                  command.run();

              }

          };

          AyncModel model = new AyncModel(immediateExecutor);

          final List<String> result = new ArrayList<>();

          model.loadAync(new AyncModel.Callback() {

              @Override

              public void onSuccess(List<String> list) {

                  result.addAll(list);

              }

              @Override

              public void onFailure(int code, String msg) {

                  fail();

              }

          });

          // 此处有坑,如果不加这行代码,就会出现Handler没有执行Runnable的问题

          ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

          assertEquals(1, result.size());

      }

  }

  不知你有没有感觉到,使用Executor方式之后,不管是源代码还是测试代码看起来都很清爽!

 (3)、使用AsyncTask

  Android提供AsyncTask类,很方便我们进行异步操作,初学Android时,很喜欢这种方式。进行单元测试时,建议使用 AsyncTask.executeOnExecutor(),而不是直接使用AsyncTask.execute(),通过依赖注入的方式,在测试环境下将同步的Executor传进去进去。

 (4)、使用RxJava

  这个是不得不提的一种方法,鉴于强大的线程切换功能,越来越多的人使用RxJava来做异步操作,RxJava代码的单元测试也是经常被问到的一个问题。不管你是否用到RxJava,反正我现在的项目就用到了。至于如何将异步操作切换到同步执行,之前已经详细讲到了,可以回到上面再看看。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容