Android异步函数单元测试

大部分内容也适合Java,此处主要是对sdk类别的module做unit test,不涉及UI

1. 配置

1) 对于涉及到android原生的类库返回默认的对象,否则,之后遇到Log之类的语句都需要手动mock,但是不要期待这个配置对android API提供全面的支持

testOptions {
    unitTests.returnDefaultValues = true
}

 参考这边的说明 Unit testing support

"Method ... not mocked."

The android.jar file that is used to run unit tests does not contain any actual code - that is provided by the Android system image on real devices. Instead, all methods throw exceptions (by default). This is to make sure your unit tests only test your code and do not depend on any particular behaviour of the Android platform (that you have not explicitly mocked e.g. using Mockito). If that proves problematic, you can add the snippet below to your build.gradle to change this behavior:

android {
  // ...
  testOptions { 
    unitTests.returnDefaultValues = true
  }
}
We are aware that the default behavior is problematic when using classes like Log or TextUtils and will evaluate possible solutions in future releases.

2) 配置依赖库

testCompile 'junit:junit:4.12'
testCompile "org.mockito:mockito-core:1.10.+"
testCompile ('org.powermock:powermock-api-mockito:1.6.3') {
    exclude module: 'hamcrest-core'
    exclude module: 'objenesis'
}
testCompile ('org.powermock:powermock-module-junit4:1.6.3') {
    exclude module: 'hamcrest-core'
    exclude module: 'objenesis'
}

 其中powermock用于对static和final方法提供mock支持

mockito site

powermock site

另外也有人对powermock提出了质疑,大致的理由是,为了实现对static和final方法的mock,powermock在jvm中对被测试类的字节码进行了部分修改(只是在测试运行时进行了修改,并不会影响你的源码),所以实际测试的类和你自己写的是有部分区别的,而且,进一步就引发了对static用法的质疑,参见:

PowerMock + Mockito VS Mockito alone

Why is wide usage of PowerMock problematic

3) 设置Android Studio

左边栏Build Variants -> Test Artifact 选择 Unit Tests

2. 对于单进程的异步请求单元测试

首先有如下的类

public class JustAClass {
    //callback接口
    public interface JustACallBack {
        void callFunc(JustAResult result);
    }

    //callback中携带的数据父接口
    public interface JustAResult {}

    //用于测试的数据类
    public class ImplementedResult implements JustAResult{
        public ImplementedResult(String content) {this.content = content;}
        public String content;
    }

    //此处的测试函数
    public void mainThreadFunc(final JustACallBack callBack) {
        callBack.callFunc(new ImplementedResult("can you reach me"));
    }
}

 Mockito提供了ArgumentCaptor用于获取异步回调的结果

public class JustAClassTest {

    @Captor
    ArgumentCaptor<JustAClass.JustAResult> captor;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testMainThreadFunc() throws Exception {

        JustAClass justAClass = new JustAClass();

        JustAClass.JustACallBack callBack = Mockito.mock(JustAClass.JustACallBack.class);

        justAClass.mainThreadFunc(callBack);

        Mockito.verify(callBack).callFunc(captor.capture());

        Assert.assertEquals(((JustAClass.ImplementedResult) captor.getValue()).content, "can you reach me");
    }
}

3. 如果被测试函数启动了一个子线程,然后在子线程中调用回调接口,那么上面的测试是无效的

一般mockito会报错 Wanted but not invoked,出错点在

Mockito.verify(callBack).callFunc(captor.capture());

原因是子线程尚未结束,而主进程已经开始去校验结果了。

首先被测试的类

public class JustAClass {
    //callback接口
    public interface JustACallBack {
        void callFunc(JustAResult result);
    }

    //callback中携带的数据父接口
    public interface JustAResult {}

    //用于测试的数据类
    public class ImplementedResult implements JustAResult{
        public ImplementedResult(String content) {this.content = content;}
        public String content;
    }

    //此处的测试函数
    public void mainThreadFunc(final JustACallBack callBack) {
        callBack.callFunc(new ImplementedResult("can you reach me"));
    }

    //此处的测试函数
    public void subThreadFunc(final JustACallBack callBack) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //模拟耗时操作0.5s
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                callBack.callFunc(new ImplementedResult("can you reach me"));
            }
        }).start();
    }
}

 此时可以通过加锁的方式来等待子线程结束,这边用的CountDownLatch

CountDownLatch is initialized with a given count. The await methods block until the current count reaches zero due to invocations of the countDown() method, after which all waiting threads are released and any subsequent invocations of await return immediately. This is a one-shot phenomenon -- the count cannot be reset.

public class JustAClassTest {

    @Captor
    ArgumentCaptor<JustAClass.JustAResult> captor;
    CountDownLatch latch;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        //for child thread async
        latch = new CountDownLatch(1);
    }

    @Test
    public void testMainThreadFunc() throws Exception {

        JustAClass justAClass = new JustAClass();

        JustAClass.JustACallBack callBack = Mockito.mock(JustAClass.JustACallBack.class);

        justAClass.mainThreadFunc(callBack);

        Mockito.verify(callBack).callFunc(captor.capture());

        Assert.assertEquals(((JustAClass.ImplementedResult) captor.getValue()).content, "can you reach me");
    }

    @Test
    public void testSubThreadFunc() throws Exception {
        JustAClass justAClass = new JustAClass();

        justAClass.subThreadFunc(new JustAClass.JustACallBack() {
            @Override
            public void callFunc(JustAClass.JustAResult result) {
                Assert.assertTrue(result instanceof JustAClass.ImplementedResult);

                JustAClass.ImplementedResult resultReal = (JustAClass.ImplementedResult) result;

                Assert.assertEquals("can you reach me", resultReal.content);

                latch.countDown();
            }
        });

        //wait 1s maximum before callback returns
        latch.await(1000, TimeUnit.MILLISECONDS);
    }
}

相关推荐