美文网首页单元测试
AndroidStudio中使用Junit进行单元测试

AndroidStudio中使用Junit进行单元测试

作者: Ziv_紫藤花开 | 来源:发表于2017-09-24 13:30 被阅读3228次

    单元测试

    Unit Testing,是指对软件中的最小可测试单元进行检查和验证。

    误解

    1. 编写单元测试没有用并且浪费大量的开发时间,延迟开发进度
    2. 从没写过,不会写,不影响产品功能

    实际

    好的测试能避免开发中遇到的80%以上奇奇怪怪的问题
    促进编写出模块化、松耦合高内聚的优质代码,减少代码重构

    测试框架

    AndroidJUnitRunner:兼容JUnit 4测试运行器
    Espresso:UI测试框架;适合在单个应用的功能UI测试
    UI Automator:UI测试框架;适用于跨应用的功能UI测试及安装应用

    AndroidJunitRunner

    Enviroment搭建

    android {
        defaultConfig {
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
    }
    
    dependencies {
        androidTestCompile 'com.android.support:support-annotations:23.1.1'
        androidTestCompile 'com.android.support.test:runner:0.4.1'
        androidTestCompile 'com.android.support.test:rules:0.4.1'
    }
    

    Tips关键

    InstrumentationRegistry.getInstrumentation()返回当前正在运行的Instrumentation
    InstrumentationRegistry.getContext()返回此Instrumentation软件包的上下文。
    InstrumentationRegistry.getTargetContext()返回目标应用的应用上下文。
    InstrumentationRegistry.getArguments()返回传递给此Instrumentation的参数Bundle。

    当测试使用JUnit4时,需要注解@RunWith(AndroidJUnit4.class)
    @Before:测试方法每次执行Test方法之前都会执行的方法注解,该注解替代了JUnit 3中的setUp()方法。
    @Test:测试方法体注解
    @After:测试方法每次执行完一个Test方法后都会执行的方法注解,该注解替代了JUnit 3中的tearDown()方法。
    @Rule: 简单来说,是为各个测试方法提供一些支持。具体来说,比如我需要测试一个Activity,那么我可以在@Rule注解下面采用一个ActivityTestRule,该类提供了对相应Activity的功能测试的支持。该类可以在@Before和@Test标识的方法执行之前确保将Activity运行起来,并且在所有@Test和@After方法执行结束之后将Activity杀死。在整个测试期间,每个测试方法都可以直接对相应Activity进行修改和访问。
    @BeforeClass: 为测试类标识一个static方法,在测试之前只执行一次。
    @AfterClass: 为测试类标识一个static方法,在所有测试方法结束之后只执行一次。
    @Test(timeout=<milliseconds>): 为测试方法设定超时时间。

    @RequiresDevice:物理设备上运行。
    @SdkSupress:限定最低SDK版本。例如@SDKSupress(minSdkVersion=18)。
    @SmallTest,@MediumTest和@LargeTest:测试分级。

    Demo示例

    不管是继承AndroidTestCase还是ActivityInstrumentationTestCase2,在Android中都显示方法已经过时,因此我们什么都不继承,比如

    package com.ziv.zutils;
    
    import android.content.Context;
    import android.support.test.InstrumentationRegistry;
    import android.support.test.filters.LargeTest;
    import android.support.test.filters.MediumTest;
    import android.support.test.filters.RequiresDevice;
    import android.support.test.filters.SdkSuppress;
    import android.support.test.filters.SmallTest;
    import android.support.test.runner.AndroidJUnit4;
    
    import org.junit.After;
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    
    import static org.junit.Assert.assertEquals;
    
    /**
     * Instrumentation test, which will execute on an Android device.
     *
     * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
     */
    @RunWith(AndroidJUnit4.class)
    public class ExampleInstrumentedTest {
        @Before
        public void testBefore() throws Exception {
            LogUtil.e("JUnit","testBefore");
        }
    
        @Test
        public void useAppContext() throws Exception {
            // Context of the app under test.
            Context appContext = InstrumentationRegistry.getTargetContext();
            LogUtil.e("JUnit","testTest");
            assertEquals("com.ziv.zutils", appContext.getPackageName());
        }
    
        @Test
        public void testTest() throws Exception {
            LogUtil.e("JUnit","testTest");
        }
    
        @Test
        @SmallTest
        public void testTestSmallTest() throws Exception {
            LogUtil.e("JUnit","testTestSmallTest");
        }
    
        @SmallTest
        public void testSmallTest() throws Exception {
            LogUtil.e("JUnit","testSmallTest");
        }
    
        @MediumTest
        public void testMediumTest() throws Exception {
            LogUtil.e("JUnit","testMediumTest");
        }
    
        @LargeTest
        public void testLargeTest() throws Exception {
            LogUtil.e("JUnit","testLargeTest");
        }
    
        @RequiresDevice
        public void testRequiresDevice() throws Exception {
            LogUtil.e("JUnit","testRequiresDevice");
        }
    
        @SdkSuppress(minSdkVersion = 19)
        public void testSdkSuppress() throws Exception {
            LogUtil.e("JUnit","testSdkSuppress");
        }
    
        @After
        public void testAfter() throws Exception {
            LogUtil.e("JUnit","testAfter");
        }
    }
    

    使用adb命令进行测试,分成10个碎片,仅执行第二片测试,请使用:

    adb shell am instrument -w -e numShards 10 -e shardIndex 2
    

    Espresso

    Enviroment搭建

    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
    另外还需要hamcrest的库,用来和Espresso配合使用
    androidTestCompile 'org.hamcrest:hamcrest-library:1.3'

    Tips关键

    Run-->Record Espresso Test

    1. onView()找元素
    2. perform()操作元素
    3. check()检查结果

    Demo示例

    API参考:developer.android.com/reference/android/support/test/package-summary.html
    测试参考:http://developer.android.com/training/testing/ui-testing/espresso-testing.html

    UI Automator

    Enviroment搭建

    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'

    Tips关键

    UI Automator仅支持Android 4.3(API Level 18)及以上版本。
    使用流程

    1. 获得一个UiDevice对象,代表我们正在执行测试的设备。该对象可以通过一个getInstance()方法获取,入参为一个Instrumentation对象:
      UiDevice mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
    2. 通过findObject()方法获取到一个UiObject对象,代表我们需要执行测试的UI组件。
    3. 对该UI组件执行一系列操作。
    4. 检查操作的结果是否符合预期。

    Demo示例

    API参考:http://developer.android.com/reference/android/support/test/package-summary.html
    实例:http://developer.android.com/training/testing/ui-testing/uiautomator-testing.html

    谷歌安卓测试实例介绍

    AndroidJunitRunnerSample
    实例下载地址:https://github.com/googlesamples/android-testing/tree/master/runner/AndroidJunitRunnerSample

    高效单元测试

    代码的可读性

    1. 使用更易懂的API,如
    //代码一
    String msg = “hello,World”;
    assertTrue(msg.indexOf(“World”)!=-1);
    //代码二
    String msg = “hello,World”;
    assertThat(msg.contains(“World”),equals(true));
    

    明显代码二的逻辑更符合人类思想,更容易理解

    1. 避免使用较底层的方式,比如位运算符
    //代码一
    assertTrue(Platform.IS_32_BIT ^ Platform.IS_64_BIT);
    //代码二
    assertTrue("Not 32 or 64-bit platform?", Platform.IS_32_BIT || Platform.IS_32_BIT);
            assertFalse("can't be 32 and 64-bit at the same time.",Platform.IS_32_BIT && Platform.IS_32_BIT);
    

    当99%的人类可以在3s内判断出代码一是在做什么的时候,我们就可以使用代码一,而不是逻辑更清楚的代码二了

    代码的可维护性

    1. 不要在测试代码中运用防御性策略
    Data data = project.getData();
    //代码一
    assertNotNull(data);// 没有任何实际意义
    assertEquals(4,data.count());
    //代码二
    assertNotNull(data);// 可测试出data.getSummary()是否为空的情况
    assertEquals(4,data.getSummary().getTotal())
    
    1. 减少重复性复制粘贴的代码
    //代码一
    public class TemplateTest(){
         @Test
        public void emptyTemplate() throws Exception{
            String template=“”;
            assertEquals(template,new Template(template).getType());
       }
        @Test
        public void plainTemplate() throws Exception{
            String template=“plaintext”;
            assertEquals(template,new Template(template).getType());
      }
    }
    //代码二
    public class TemplateTest(){
         @Test
        public void emptyTemplate() throws Exception{
            assertTemplateType(“”);
        }
        @Test
        public void plainTemplate() throws Exception{
            assertTemplateType(“plaintext”);
        }
       private void assertTemplateType(String template){
          assertEquals(template,newTemplate(template).getType())
       }
    }
    
    1. 避免由于条件逻辑而造成的测试遗漏,存在条件逻辑时要在最后加上 fail()方法,强制测试失败
    //代码一
    public class DictionaryTest{ 
    @Test
    public void testDictionary() throws Exception{
        Dictionary dict = new Dictionary();
        dict.add(“A”,new Long(3));
        dict.add(“B”,”21”);
        for(Iterator e = dict.iterator();e.hasNext()){
            Map.Entry entry = (Map.Entry) e.next();
            if(“A”.equals(entry.getKey()))
                asserEquals(3L,entry.getValue());
            if(“B”.equals(entry.getKey()))
                assertEquals(“21”),entry.getValue();
         }
      }
    }
    //代码二
    public class DictionaryTest{ 
    @Test
    public void testDictionary() throws Exception{
        Dictionary dict = new Dictionary();
        dict.add(“A”,new Long(3));
        dict.add(“B”,”21”);
        assertContain(dict.iterator(),”A”,3L);
            assertContain(dict.iterator(),”B”,21);
      }
    private void assertContain(Iterator i,Object key,Object value){
            while(i.hasNext()){
                Map.Entry entry = (Map.Entry)i.next();
                if(key.equals(entry.getKey())){
                    assertEquals(value,entry.getValue());
                   return;
                }
            }
            fail("Iterator didn't contain "+ key);
        }
    }
    

    代码一当Iterator为空时,测试并不会失败,这并不符合我们单元测试的目的

    1. 避免使用sleep方法浪费大量的测试时间
      counterAccessFromMultipleThreads 用来测试一个多线程计数器,开启10个线程,每个线程调用计数器1000次,sleep(500),是为了让主线程等待开启的10个线程执行完毕
      那么问题来了,如果在10毫秒内所有线程都执行完毕,岂不白白浪费了490毫秒?又或者在等待500毫秒后仍有线程没有执行完毕,那该怎么办?
    @Test
    public class counterAccessFromMultipleThreads{
      final Counter counter = new Counter();
      final int callsPerThread = 1000;//每个线程调用计数器1000次
      final Set<Long> values = new HashSet<Long>();
      Runnable runnable = new Runnable(){
          public void run(){
              for(int i=0;i<callsPerThread;i++){
                  values.add(counter.getAndIncrement());
              }
          }
      }; 
      int threads = 10;//开启10个线程
      for(int i=0;i<threads;i++){
          new Thread(runnable).start();
      }
      Thread.sleep(500);
      int exceptedNoOfValues = threads * callsPerThread;
      assertEquals(exceptedNoOfValues ,values.size());
    }
    

    改进后的测试方法:

    public class counterAccessFromMultipleThreads{
      final Counter counter = new Counter();
      final int callsPerThread = 1000;
      final int numberOfthreads = 10;
      final CountDownLatch allThreadsComplete = new CountDownLatch(numberOfthreads);
      final Set<Long> values = new HashSet<Long>();
      Runnable runnable = new Runnable(){
          public void run(){
              for(int i=0;i<callsPerThread;i++){
                  values.add(counter.getAndIncrement());
              }
              allThreadsComplete.countDown();
          }
      }; 
    
    for(int i=0;i<numberOfthreads;i++){
          new Thread(runnable).start();
      }
      allThreadsComplete.await();
      //  allThreadsComplete.await(10,TimeUnit.SECONDS);
      int exceptedNoOfValues = threads * callsPerThread;
      assertEquals(exceptedNoOfValues ,values.size());
    }
    

    等待所有线程结束后再继续执行,有更好的办法,java.util.concurrent 包中的CountDownLatch类完全可以胜任这项工作。
    调用await方法开始阻塞,直到所有的线程都通知完成,然后继续执行主线程代码。也可以设置超时时间,allThreadsComplete.await(10,TimeUnit.SECONDS); 如果10秒钟内子线程仍未执行结束,也会继续执行主线程。

    1. 避免歧义注释
    /**
         * 功能描述: 发送邮件<br>
         * 〈功能详细描述〉
         * @return
         * @see [相关类/方法](可选)
         * @since [产品/模块版本](可选)
         */
      public void sendShortMessage() {
          //todo
       }
      // 代码二
      public void sendEmail() {
          //todo
       }
    

    sendShortMessage不是说发送短信么?注释又写发送邮件…这样的注释真不如不要写了…

    1. 避免永不失败的测试
    //代码一
    @Test
    public void includeForMissingResourceFails()
        try{
            new Environment().include("somethingthatdoesnotexist");
           }catch(IOException e){
            assertThat(e.getMesssage(),contians(“FileNotExist”));
    }
    //代码二
    public void includeForMissingResourceFails()
        try{
            new Environment().include(“FileNotEixst”);
            // 除非抛出期望的异常,否则测试失败
            fail();
           }catch(IOException e){
            assertThat(e.getMesssage(),contians(“FileNotExist”));
    }
    

    代码一中当程序发生异常时,异常被catch捕获,测试通过。程序没有发生异常时,程序正常执行完毕,测试也是通过的,发现不了问题

    遵守的原则

    1. 少用继承多用组合,继承更大程度上是为了多态而非复用代码
    2. 单元测试应该模块化,每个模块小而专注,减少反馈链
    3. 单一职责,如果一个单元测试方法失败了,那么导致它失败的原因只有一个
    4. 加载外部文件时使用相对路径而不是绝对路径
    5. 见名知意的定义常量,而不是魔法数字
    6. 完整的方法注释,说明测试的内容,使用的方法,避免注释歧义等

    测试驱动开发

    TDD测试驱动开发.png

    参考资料:
    https://my.oschina.net/u/1433482/blog/602003
    https://segmentfault.com/a/1190000004338384
    http://www.cnblogs.com/jarman/p/5272761.html

    相关文章

      网友评论

        本文标题:AndroidStudio中使用Junit进行单元测试

        本文链接:https://www.haomeiwen.com/subject/ddtrkxtx.html