Android之单元测试

申明

单元测试的目的以及测试内容

  • 目的当然是提前找出问题,提高程序健壮性
  • 单元测试要测什么?
  1. 列出想要测试覆盖的正常、异常情况,进行测试验证;
  2. 性能测试,例如某个算法的耗时等等。
  • 单元测试的分类
  1. 本地测试(Local tests): 只在本地机器JVM上运行,以最小化执行时间,这种单元测试不依赖于Android框架,或者即使有依赖,也很方便使用模拟框架来模拟依赖,以达到隔离Android依赖的目的,模拟框架如google推荐的[Mockito][1];
  2. 仪器化测试(Instrumented tests): 在真机或模拟器上运行的单元测试,由于需要跑到设备上,比较慢,这些测试可以访问仪器(Android系统)信息,比如被测应用程序的上下文,一般地,依赖不太方便通过模拟框架模拟时采用这种方式。

JUnit 注解

  • 了解一些JUnit注解,有助于更好理解后续的内容。


本地测试

  • 根据单元有没有外部依赖(如Android依赖、其他单元的依赖),将本地测试分为两类,首先看看没有依赖的情况
添加依赖
 //单元测试的依赖库
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    // Optional -- Mockito framework(可选,用于模拟一些依赖对象,以达到隔离依赖的效果)
    androidTestImplementation 'org.mockito:mockito-core:2.19.0'
单元测试代码位置
  • java.package目录下是仪器化单元测试,UI测试
  • test.java.package目录下是本地单元测试


    图片.png
创建测试类
  • 手动创建也可以自动生成
  • 在类名上ALT+ENTER弹出选择选项


    图片.png
- 其中勾选setUp/tearDown是会自动创建一个setUp()/ tearDown() 的空方法
public class TimeUtilsTest extends TestCase {

    public void setUp() throws Exception {
        super.setUp();
    }

    public void tearDown() throws Exception {
    }
}
简单例子
  • 判断谁否是收集号码的方法
public class PhoneUtils {

    /**
     * 判断是否是手机号
     */
    public static boolean assertPhone(String phoneNum){
        return phoneNum.matches("^1[3-9][0-9]{9}");
    }

    public static boolean assertPhone2(String phoneNum){
        return phoneNum.matches("^1[3-9]\\d{9}");
    }
}
  • 单元测试用例和效果
  1. 正确输入和反馈
   public void testAssertPhone() {
//        assertEquals(PhoneUtils.assertPhone("12871650177"),true);
        assertEquals(PhoneUtils.assertPhone("13871650177"),true);
    }
测试通过
  1. 错误输入和反馈
    public void testAssertPhone() {
        assertEquals(PhoneUtils.assertPhone("12871650177"),true);
//        assertEquals(PhoneUtils.assertPhone("13871650177"),true);
    }
测试失败
  • 可以很直观的告诉你测试是否通过,即你运行的结果和你的预期是否符合
  • 其中使用中出现的一个问题:
Method d in android.util.Log not mocked. See http://g.co/androidstudio/not-mocked for details.
java.lang.RuntimeException: Method d in android.util.Log not mocked. See http://g.co/androidstudio/not-mocked for details.
    at android.util.Log.d(Log.java)
    at com.southwind.module_common.SouthWindLog.d(SouthWindLog.java:51)
    at com.southwind.module_common.SouthWindLogTest.testTestD1(SouthWindLogTest.java:39)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        ...

//上述错误的原因是方法里面调用了android.util.Log包里面的静态方法
//解决办法就是在你自动生成的before方法里面mock一下这个静态类
   @Override
    protected void setUp() throws Exception {
        super.setUp();
        mockStatic(Log.class);
    }
//再次运行就能正常了,这个就是接下来要说的隔离依赖;

通过模拟框架模拟依赖,隔离依赖

  • 前面验证邮件格式的例子,本地JVM虚拟机就能提供足够的运行环境,但如果要测试的单元依赖了Android框架,比如用到了Android中的Context类的一些方法,又比如上述问题中的Log类的调用,本地JVM将无法提供这样的环境,这时候模拟框架[Mockito][1]就派上用场了。
Mockito的使用
  • 首先是添加依赖
    // required if you want to use Mockito for unit tests
    testCompile 'org.mockito:mockito-core:2.7.22'
    // required if you want to use Mockito for Android tests
    androidTestCompile 'org.mockito:mockito-android:2.7.22'
  • 然后是使用Mockito创建mock对象
  1. 使用静态方法mock()
  2. 使用注解@Mock
使用注解
    @Mock
    Activity activityMock;
    @Rule
    public MockitoRule rule = MockitoJUnit.rule();

    @Test
    public void testD() {
        SouthWindLog.d(activityMock,"测试");
    }
  • 啊,使用失败了哈哈哈....这里的activityMock总是null
使用静态方法哈
   @Mock
    Activity activityMock;
    @Test
    public void testD() {
        activityMock = Mockito.mock(Activity.class);
        when(activityMock.getString(R.string.log_d)).thenReturn("单元测试日志");
        SouthWindLog.d(activityMock);
        //verify是判断方法是否被调用过
        verify(activityMock).getString(R.string.log_d);
    }
  • 这个倒是成功了,但是说实话这个测试其实测试了个寂寞,因为它没有具体返回,其实不好判断,道理运行正确与否;

配置模拟对象

  • Mockito可以通过自然的API来实现模拟对象的返回值.没有指定的方法调用返回空值
"when thenReturn"和"when thenThrow"
  • 模拟对象可以根据传入方法中的参数来返回不同的值, when(….).thenReturn(….)方法是用来根据特定的参数来返回特定的值.
//强行指定1和2是相等的。
 public void testMockObject(){
        PhoneUtils phoneUtils = mock(PhoneUtils.class);
        when(phoneUtils.isEqules(1,2)).thenReturn(true);
        assertEquals(phoneUtils.isEqules(1,2),false);
    }
//结果就是不通过哈哈
//返回多个值
        Iterator<Integer> i = mock(Iterator.class);
        when(i.next()).thenReturn(11).thenReturn(12);
        String res = i.next() + "/" + i.next();
        assertEquals(res,"11/12");
  • when() thenThrow()可以用来抛出异常;当然运行就抛出自己定义的异常了;
  PhoneUtils phoneUtils = mock(PhoneUtils.class);
        when(phoneUtils.divideInput(0)).thenThrow(new Exception("0不能做除数"));
        try {
            phoneUtils.divideInput(0);
            fail("0做除数啦,夭寿啦");
        }catch (Exception e){
        }
"doReturn when" 和 "doThrow when"
  • 这个和上面的差不多;
使用Spy包装实例
  • 可以使用@Spy注解 或者 spy() 方法来包装一个真实的对象. 除非有特殊的指定,否则每次调用都会委托给该对象
 public void testSpy(){
        ArrayList<String> list = new ArrayList<>();
        ArrayList<String> spy = Mockito.spy(list);
//        when(spy.get(0)).thenReturn("第一个元素");
//        assertEquals("第一个元素",spy.get(0));

        doReturn("第一个元素").when(spy).get(0);
        assertEquals("第一个元素",spy.get(0));
    }
  • 注意:在使用Spy包装真实对象时使用when(….).thenReturn(….)将无效,必须使用 doReturn(…).when(…)来进行插桩.上面注释掉的部分使用直接报错了;
验证模拟对象的调用
  • Mockito将会追踪所有方法的调用和传入模拟对象的参数.你可以在模拟对象上使用verify()方法验证指定的条件是否满足.例如,你可以验证是否使用某些参数调用了方法.这种测试称为行为测试.行为测试并不能检查方法调用的结果,但是它可以验证一个方法是否使用正确的参数被调用
    public void testVerify(){
        PhoneUtils phoneUtils = mock(PhoneUtils.class);
        when(phoneUtils.divideInput(5)).thenReturn(2);

        phoneUtils.divideInput(10);
        phoneUtils.isEqules(1,2);
        phoneUtils.isEqules(2,2);
        phoneUtils.isEqules(2,2);


        verify(phoneUtils).divideInput(ArgumentMatchers.eq(10));//期望入参是10的调用
        //verify(phoneUtils).divideInput(ArgumentMatchers.eq(5));//报错了,没有调用入参是5的情况

        //判断方法跑了多少次
        verify(phoneUtils,times(2)).isEqules(2,2);
        //verify(phoneUtils,times(3)).isEqules(2,2);//这里报错,说跑了两次,但是期望的是三次

        //判断是否从没出现后面的调用情况
        verify(phoneUtils,never()).isEqules(0,1);
        verify(phoneUtils, atLeastOnce()).isEqules(1,2);
        verify(phoneUtils, atLeast(1)).isEqules(1,2);//至少被调用1次
        verify(phoneUtils, atMost(3)).isEqules(1,2);//至多调用了3次

        //下面的方法用来检查是否所有的用例都涵盖了,如果没有将测试失败
        //放在所有的测试后面
        verifyNoMoreInteractions(phoneUtils);
    }
Answer
  • 在写测试用例时针对复杂的方法结果往往会使用Answer.虽然使用thenReturn可以每次返回一个预定义的值,但是通过answers可以让你的插桩方法(stubbed method)根据参数计算出结果.
  • 其实原作者的没怎么看懂,看起来和doReturen..when差不多。可能是我还没有理解吧;
  • 没问题了,只要跑一次就知道了使用doReturn..when是会报错的,但是answer能通过测试
  • 还发现了一点,所有的设定是会被最新的覆盖的,下面的输出是first,所以前面不论设置了多少answer只有最后一个有效哈;
public void testAnswer(){
        TestAnswer testAnswer = mock(TestAnswer.class);
        doAnswer(returnsFirstArg()).when(testAnswer).add(anyString(),anyString());
//        doReturn(returnsFirstArg()).when(testAnswer).add(anyString(),anyString());
        when(testAnswer.add(anyString(),anyString())).thenAnswer(returnsSecondArg());
        when(testAnswer.add(anyString(),anyString())).then(returnsFirstArg());
        System.out.println(testAnswer.add("first","second"));
    }
  • 有时候会传入回调方法作为参数
   public void testAnswer() {
        TestAnswer testAnswer = mock(TestAnswer.class);
//        doAnswer(returnsFirstArg()).when(testAnswer).add(anyString(),anyString());
////        doReturn(returnsFirstArg()).when(testAnswer).add(anyString(),anyString());
//        when(testAnswer.add(anyString(),anyString())).thenAnswer(returnsSecondArg());
//        when(testAnswer.add(anyString(),anyString())).then(returnsFirstArg());
//        System.out.println(testAnswer.add("first","second"));

        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                //invocation这个是存放你参数的容易,我第三个参数是回调函数,所以下面下标要写2
                CallBack callBack = invocation.getArgument(2);
                callBack.back("callback");
                return "return";
            }
        }).when(testAnswer).add(anyString(), anyString(), any(CallBack.class));

        String result = testAnswer.add("1", "2", new CallBack() {
            @Override
            public void back(String back) {
                System.out.println(back);
            }
        });
        System.out.println(result);
    }

    class TestAnswer {
        public String add(String first, String second, CallBack callBack) {
            callBack.back("");
            return "";
        }

        public String add(String first, String second) {
            return "";
        }

    }

    interface CallBack {
        void back(String back);
    }
//最后打印的是 符合我再模拟中提供的数据
...
...
> Task :module_common:testDebugUnitTest
callback
return
BUILD SUCCESSFUL in 2s
16 actionable tasks: 3 executed, 13 up-to-date
  • 上面注意的是when...xxx方法都是建立再原方法是有返回的基础上,没有返回就不能这么写;

总结

  • 写起来还挺费劲的。
  • 考虑可读性:对于方法名使用表达能力强的方法名,对于测试范式可以考虑使用一种规范, 如 RSpec-style。方法名可以采用一种格式,如: [测试的方法][测试的条件][符合预期的结果]。
  • 不要使用逻辑流关键字:比如(If/else、for、do/while、switch/case),在一个测试方法中,如果需要有这些,拆分到单独的每个测试方法里。
  • 测试真正需要测试的内容:需要覆盖的情况,一般情况只考虑验证输出(如某操作后,显示什么,值是什么)。
  • 不需要考虑测试private的方法:将private方法当做黑盒内部组件,测试对其引用的public方法即可;不考虑测试琐碎的代码,如getter或者setter。
  • 每个单元测试方法,应没有先后顺序:尽可能的解耦对于不同的测试方法,不应该存在Test A与Test B存在时序性的情况。
  • 感觉单元测是麻烦的很,其实普通开发哪有写这东西,都是丢给测试直接真机测试。不过对于数据准确性要求很高的这个还是很有用的,比如零售相关;还有那种计算多,数据复杂的也很好;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容