Android单元测试

作者: anmi7 | 来源:发表于2017-03-30 13:39 被阅读352次

    一.基本介绍

    背景:

    目前处于高速迭代开发中的Android项目往往需要除黑盒测试外更加可靠的质量保障,这正是单元测试的用武之地。单元测试周期性对项目进行函数级别的测试,在良好的覆盖率下,能够持续维护代码逻辑,从而支持项目从容应对快速的版本更新。

    正是由于测试在开发中的重要地位,才会在IT界刮起了 TDD 的旋风。TDD,也就是测试驱动开发模式。它旨在强调在开发功能代码之前,先编写测试代码。也就是说在明确要开发某个功能后,首先思考如何对这个功能进行测试,并完成测试代码的编写,然后编写相关的代码满足这些测试用例。然后循环进行添加其他功能,直到完成全部功能的开发。

    二.Java 测试工具(框架)

    1.JUnit(推荐使用JUnit4)
    JUnit 在日常开发中还是很常用的,而且 Java 的各种 IDE (Eclipse、MyEclipse、IntelliJ IDEA)都集成了 JUnit 的组件。当然,自己添加插件也是很方便的。JUnit 框架是 Java 语言单元测试当前的一站式解决方案。这个框架值得称赞,因为它把测试驱动的开发思想介绍给 Java 开发人员并教给他们如何有效地编写单元测试。

    2.TestNG
    TestNG,即Testing Next Generation,下一代测试技术。是根据JUnit和NUnit思想,采用 jdk 的 annotation 技术来强化测试功能并借助XML 文件强化测试组织结构而构建的测试框架。TestNG 的强大之处还在于不仅可以用来做单元测试,还可以用来做集成测试。

    重点介绍下JUnit4

    JUnit是Java单元测试框架,已经在Eclipse中默认安装。目前主流的有JUnit3和JUnit4。JUnit3中,测试用例需要继承TestCase类。JUnit4中,测试用例无需继承TestCase类,只需要使用@Test等注解,建议使用JUnit4。

    JUnit4通过注解的方式来识别测试方法。目前支持的主要注解有:

    • @BeforeClass 全局只会执行一次,而且是第一个运行
    • @Before 在测试方法运行之前运行
    • @Test 测试方法
    • @After 在测试方法运行之后允许
    • @AfterClass 全局只会执行一次,而且是最后一个运行
    • @Ignore 忽略此方法

    @Before 该方法在每次测试方法调用前都会调用 @Test 说明了该方法需要测试 @BeforeClass 该方法在所有测试方法之前调用,只会被调用一次 @After 该方法在每次测试方法调用后都会调用 @AfterClass 该方法在所有测试方法之后调用,只会被调用一次 @Ignore 忽略该方法

    三.单元测试范围

    一般来说,单元测试任务包括

    1. 接口功能测试:用来保证接口功能的正确性。
    2. 局部数据结构测试(不常用):用来保证接口中的数据结构是正确的。 比如(1).变量有无初始值,(2).变量是否溢出.
    3. 边界条件测试
      (1).变量没有赋值(即为NULL)
      (2).变量是数值(或字符)
      -主要边界:最小值,最大值,无穷大(对于DOUBLE等)
      -溢出边界(期望异常或拒绝服务):最小值-1,最大值+1
      -临近边界:最小值+1,最大值-1
      (3). 变量是字符串
      -引用“字符变量”的边界
      -空字符串
      -对字符串长度应用“数值变量”的边界
      (4).变量是集合
      -空集合
      -对集合的大小应用“数值变量”的边界
      -调整次序:升序、降序
      (5). 变量有规律
      -比如对于Math.sqrt,给出n2-1,和n2+1的边界
      (6). 所有独立执行通路测试:保证每一条代码,每个分支都经过测试
      -代码覆盖率
      1>.语句覆盖:保证每一个语句都执行到了
      2>.判定覆盖(分支覆盖):保证每一个分支都执行到
      3>.条件覆盖:保证每一个条件都覆盖到true和false(即if、while中的条件语句)
      4>.路径覆盖:保证每一个路径都覆盖到

    -相关软件 (Cobertura:语句覆盖)

    1. 各条错误处理通路测试:保证每一个异常都经过测试

    如下是一个JUnit4的示例:

    /**
     * Created by huanming on 17/3/13.
     */
    public class Junit4TestCase {
    
        @BeforeClass
        public static void setUpBeforeClass() {
            System.out.println("Set up before class");
        }
    
        @Before
        public void setUp() throws Exception {
            System.out.println("Set up");
        }
    
        @Test
        public void testMathPow() {
            System.out.println("Test Math.pow");
            Assert.assertEquals(4.0, Math.pow(2.0, 2.0), 0.0);
        }
    
        @Test
        public void testMathMin() {
            System.out.println("Test Math.min");
            Assert.assertEquals(2.0, Math.min(2.0, 4.0), 0.0);
        }
    
        // 期望此方法抛出NullPointerException异常
        @Test(expected = NullPointerException.class)
        public void testException() {
            System.out.println("Test exception");
            Object obj = null;
            obj.toString();
        }
    
        // 忽略此测试方法
        @Ignore
        @Test
        public void testMathMax() {
            Assert.fail("没有实现");
        }
        // 使用“假设”来忽略测试方法
        @Test
        public void testAssume(){
            System.out.println("Test assume");
            // 当假设失败时,则会停止运行,但这并不会意味测试方法失败。
            Assume.assumeTrue(false);
            Assert.fail("没有实现");
        }
    
        @After
        public void tearDown() throws Exception {
            System.out.println("Tear down");
        }
    
        @AfterClass
        public static void tearDownAfterClass() {
            System.out.println("Tear down After class");
        }
    
    }
    

    运行结果:


    屏幕快照 2017-03-17 下午2.26.04.png

    四. 单元测试框架>Robolectric

    参考文章:
    http://robolectric.org
    https://github.com/robolectric/robolectric
    https://en.wikipedia.org/wiki/Unit_testing
    https://github.com/square/okhttp/tree/master/mockwebserver

    1. 介绍
      (1). Robolectric 是一个开源的framework,他们的做法是通过实现一套JVM能运行的Android代码,然后在unit test运行的时候去截取android相关的代码调用,然后转到他们的他们实现的代码去执行这个调用的过程。
      举个例子说明一下,比如android里面有个类叫TextView,他们实现了一个类叫ShadowTextView。这个类基本上实现了TextView的所有公共接口,假设你在unit test里面写到
      String text = textView.getText().toString();。在这个unit test运行的时候,Robolectric会自动判断你调用了Android相关的代码textView.getText(),然后这个调用过程在底层截取了,转到ShadowTextViewgetText实现。而ShadowTextView是真正实现了getText这个方法的,所以这个过程便可以正常执行。
      (2). 除了实现Android里面的类的现有接口,Robolectric还做了另外一件事情,极大地方便了unit testing的工作。那就是他们给每个Shadow类额外增加了很多接口,可以读取对应的Android类的一些状态。比如我们知道ImageView有一个方法叫setImageResource(resourceId),然而并没有一个对应的getter方法叫getImageResourceId(),这样你是没有办法测试这个ImageView是不是显示了你想要的image。而在Robolectric实现的对应的ShadowImageView里面,则提供了getImageResourceId()这个接口。你可以用来测试它是不是正确的显示了你想要的Image.

    2. 环境配置
      Android单元测试依旧需要JUnit框架的支持,Robolectric只是提供了Android代码的运行环境。如果使用Robolectric 3.0,依赖配置如下:

    testCompile 'junit:junit:4.12'
        testCompile('org.robolectric:robolectric:3.0') {
            exclude module: 'commons-logging'
        }
    

    Gradle对Robolectric 2.4的支持并不像3.0这样好,但Robolectric 2.4所有的测试框架均在一个包里,如果使用Robolectric 2.4,则需要如下配置:

    //这行配置在buildscript的dependencies中
    classpath 'org.robolectric:robolectric-gradle-plugin:0.14.+'
    apply plugin: 'robolectric'
    androidTestCompile 'org.robolectric:robolectric:2.4'
    

    需要注意:Android Studio小于2.0的版本,要支持单元测试需要设置“Build Variants”,路径是“View -->Tool Windows-->Build Variants”,然后设置为“Unit Tests”;当版本为2.0时,默认就支持。

    屏幕快照 2017-03-19 下午1.47.32.png
                               图2 单元测试工程位置
    

    如图1所示的绿色文件夹即是单元测试工程。这些代码能够检测目标代码的正确性,打包时单元测试的代码不会被编译进入APK中。

    Robolectric最麻烦就是下载依赖! 由于我们生活在天朝,下载国外的依赖很慢,即使有了翻墙,效果也一般。

    注意:第一次运行可能需要下载一些library,依赖库,可能需要花一点时间,这个跟unit test本身没关。

    第二种方法:maven地址指向 阿里云的地址。
    build.gradle

    allprojects {
            repositories {
                //依赖库,阿里云地址
                maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
                jcenter()
            }
        }
    

    具体原理参考: http://www.jianshu.com/p/a01628c3ea16

    五.Robolectric使用介绍

    Mock

    参考文章:
    http://www.open-open.com/lib/view/open1470724287040.html

    配置:

    testCompile 'org.mockito:mockito-core:1.9.5'
    

    说白了就是打桩(Stub)或则模拟,当你调用一个不好在测试中创建的对象时,Mock框架为你模拟一个和真实对象类似的替身来完成相应的行为。
    mock对象就是在调试期间用来作为真实对象的替代品。Mockito是Java中常见的Mock框架。

    Robolectric在文档中声称:“No Mocking Frameworks Required”:对于Robolectric的另一种可选方法是使用mock框架,比如Mockito;或者模拟出Android SDK。虽然这是个有效的方法,但基本上是应用代码的反向实现。

    Mockito虽然不能模拟final类、匿名类和Java基本类型;对于final方法和static方法,不能对其 when(…).thenReturn(…) 操作。另外mock对象,大多都需要植入到应用代码中,从而进行verify(...)操作;但应用代码中不一定有相应的set方法,如果要植入,就需要为了测试添加应用代码。

    但是, Mockito + Powermock可以解决上述的问题。

    示例:

    @Implements(HttpClient.class)
    public class ShadowHttpClient {
    
        protected static boolean isHandleError = false;
        protected static boolean isRaiseException = false;
        public static String lastRequestPath;
        public static String lastRequestData;
        public static List<String> allExecutedAction = new ArrayList<String>();
        public static List<String> allRequestData = new ArrayList<String>();
        private static ResponseObjectConvert converter;
        private static List<HttpResponseResult> responseResultList;
        private static int position = 0;
    
        @RealObject
        HttpClient httpClient;
    
        public void __constructor__(String host, int port, boolean isEncryptionEnabled) {
    
        }
    
        @Implementation
        public HttpResponseResult sendRequestGetResponse(String path, String request) {
            lastRequestPath = path;
            lastRequestData = request;
            allExecutedAction.add(path);
            allRequestData.add(request);
            if (isRaiseException) {
                throw new RuntimeException();
            }
    
            if (converter != null) {
                if (isHandleError) {
                    setResponseResultList(asList(new HttpResponseResult(FAILED, converter.convertResponse(), null)));
                } else {
                    setResponseResultList(asList(new HttpResponseResult(SUCCEEDED, converter.convertResponse(), null)));
                }
            }
    
            return responseResultList.get(position++);
        }
    
        @Implementation
        public HttpResponseResult getResponse(String path) {
            return sendRequestGetResponse(path,"");
        }
    
        public static void reset() {
            lastRequestPath = null;
            lastRequestData = null;
            allExecutedAction.clear();
            allRequestData.clear();
    
            ShadowHttpClient.converter = null;
            ShadowHttpClient.responseResultList = null;
            ShadowHttpClient.isHandleError = false;
            ShadowHttpClient.isRaiseException = false;
        }
    
        public static void setRaiseException(boolean isRaiseException) {
            ShadowHttpClient.isRaiseException = isRaiseException;
        }
    
        public static void setConverter(ResponseObjectConvert converter) {
            ShadowHttpClient.converter = converter;
        }
    
        public static void setHandleError(boolean handleError) {
            ShadowHttpClient.isHandleError = handleError;
        }
    
        public static void setResponseResultList(List<HttpResponseResult> responseResultList) {
            position = 0;
            ShadowHttpClient.responseResultList = responseResultList;
        }
    
        public interface ResponseObjectConvert {
            public String convertResponse();
        }
    

    Mock写法介绍

    对于一些依赖关系复杂的测试对象,可以采用Mock框架解除依赖,常用的有Mockito。例如Mock一个List类型的对象实例,可以采用如下方式:

    List list = mock(List.class);   //mock得到一个对象,也可以用@mock注入一个对象
    

    所得到的list对象实例便是List类型的实例,如果不采用mock,List其实只是个接口,我们需要构造或者借助ArrayList才能进行实例化。与Shadow不同,Mock构造的是一个虚拟的对象,用于解耦真实对象所需要的依赖。Mock得到的对象仅仅是具备测试对象的类型,并不是真实的对象,也就是并没有执行过真实对象的逻辑。
    Mock也具备一些补充JUnit的验证函数,比如设置函数的执行结果,示例如下:

    When(sample.dosomething()).thenReturn(someAction);
    //when(一个函数执行).thenReturn(一个可替代真实函数的结果的返回值);
    //上述代码是设置sample.dosomething()的返回值,当执行了sample.dosomething()这个函数时,
    //就会得到someAction,从而解除了对真实的sample.dosomething()函数的依赖
    

    上述代码为被测函数定义一个可替代真实函数的结果的返回值。当使用这个函数后,这个可验证的结果便会产生影响,从而代替函数的真实结果,这样便解除了对真实函数的依赖。
    同时Mock框架也可以验证函数的执行次数,代码如下:

    List list = mock(List.class);   //Mock得到一个对象
    list.add(1);                    //执行一个函数
    verify(list).add(1);            //验证这个函数的执行
    verify(list,time(3)).add(1);    //验证这个函数的执行次数
    

    在一些需要解除网络依赖的场景中,多使用Mock。比如对retrofit框架的网络依赖解除如下:

    public class MockClient implements Client {
        @Override
        public Response execute(Request request) throws IOException {
            Uri uri = Uri.parse(request.getUrl());
            String responseString = "";
            if(uri.getPath().equals("/path/of/interest")) {
                responseString = "返回的json1";//这里是设置返回值
            } else {
                responseString = "返回的json2";
            }
            return new Response(request.getUrl(), 200, "nothing", Collections.EMPTY_LIST, new TypedByteArray("application/json", responseString.getBytes()));
        }
    }
    //MockClient使用方式如下:
    RestAdapter.Builder builder = new RestAdapter.Builder();
    builder.setClient(new MockClient());
    

    这种方式下retrofit的response可以由单元测试编写者设置,而不来源于网络,从而解除了对网络环境的依赖。

    Shadow
    Robolectric的本质是在Java运行环境下,采用Shadow的方式对Android中的组件进行模拟测试,从而实现Android单元测试。对于一些Robolectirc暂不支持的组件,可以采用自定义Shadow的方式扩展Robolectric的功能。

    Robolectric定义了大量的Shadow类,修改或者扩展了Android OS类的行为。当一个Android OS类被实例化,Robolectric会搜索相应的Shadow类;如果找到了,将创建与之关联的Shadow对象。Android OS方法每次被调用时,Robolectirc确保:如果存在,Shadow类中的相应方法先被调用,这样就有机会做测试相关逻辑。这种策略可运用于所有的方法,包括static和final方法。

    @Implements(Point.class)
    public class ShadowPoint {
      @RealObject private Point realPoint;
      ...
      public void __constructor__(int x, int y) {
        realPoint.x = x;
        realPoint.y = y;
      }
    }
    

    上述实例中,@Implements是声明Shadow的对象,@RealObject是获取一个Android 对象,constructor则是该Shadow的构造函数,Shadow还可以修改一些函数的功能,只需要在重载该函数的时候添加@Implementation,这种方式可以有效扩展Robolectric的功能。
    Shadow是通过对真实的Android对象进行函数重载、初始化等方式对Android对象进行扩展,Shadow出来的对象的功能接近Android对象,可以看成是对Android对象一种修复。自定义的Shadow需要在config中声明,声明写法是@Config(shadows=ShadowPoint.class)。

    常见Robolectric用法

    Robolectric支持单元测试范围从Activity的跳转、Activity展示View(包括菜单)和Fragment到View的点击触摸以及事件响应,同时Robolectric也能测试Toast和Dialog。对于需要网络请求数据的测试,Robolectric可以模拟网络请求的response。对于一些Robolectric不能测试的对象,比如ConcurrentTask,可以通过自定义Shadow的方式现实测试。下面将着重介绍Robolectric的常见用法。

    1. Activity展示测试与跳转测试
      创建网络请求后,便可以测试Activity了。测试代码如下:
    @Test
    public void testSampleActivity(){
        SampleActivity sampleActivity=Robolectric.buildActivity(SampleActivity.class).
                    create().resume().get();
        assertNotNull(sampleActivity);
        assertEquals("Activity的标题", sampleActivity.getTitle());
    }
    

    Robolectric.buildActivity()用于构造Activity,create()函数执行后,该Activity会运行到onCreate周期,resume()则对应onResume周期。assertNotNull和assertEquals是JUnit中的断言,Robolectric只提供运行环境,逻辑判断还是需要依赖JUnit中的断言。
    Activity跳转是Android开发的重要逻辑,其测试方法如下:

     @Test
        public void testMainActivity() {
            MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
            mainActivity.findViewById(R.id.textView1).performClick();
    
            Intent expectedIntent = new Intent(mainActivity, SecondActivity.class);
            ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity);
            Intent actualIntent = shadowActivity.getNextStartedActivity();
            Assert.assertEquals(expectedIntent, actualIntent);
        }
    
    1. Dialog和Toast测试
      测试Dialog和Toast的方法如下:
    public void testDialog(){
        Dialog dialog = ShadowDialog.getLatestDialog();
        assertNotNull(dialog);
    }
    public void testToast(String toastContent){
        ShadowHandler.idleMainLooper();
        assertEquals(toastContent, ShadowToast.getTextOfLatestToast());
    }
    

    上述函数均需要在Dialog或Toast产生之后执行,能够测试Dialog和Toast是否弹出。

    Fragment展示与切换
    Fragment是Activity的一部分,在Robolectric模拟执行Activity过程中,如果触发了被测试的代码中的Fragment添加逻辑,Fragment会被添加到Activity中。
    需要注意Fragment出现的时机,如果目标Activity中的Fragment的添加是执行在onResume阶段,在Activity被Robolectric执行resume()阶段前,该Activity中并不会出现该Fragment。采用Robolectric主动添加Fragment的方法如下:

    @Test
    public void addfragment(Activity activity, int fragmentContent){
        FragmentTestUtil.startFragment(activity.getSupportFragmentManager().findFragmentById(fragmentContent));
        Fragment fragment = activity.getSupportFragmentManager().findFragmentById(fragmentContent);
        assertNotNull(fragment);
    }
    

    startFragment()函数的主体便是常用的添加fragment的代码。切换一个Fragment往往由Activity中的代码逻辑完成,需要Activity的引用。
    控件的点击以及可视验证

    @Test
    public void testButtonClick(int buttonID){
        Button submitButton = (Button) activity.findViewById(buttonID);
        assertTrue(submitButton.isEnabled());
        submitButton.performClick();
        //验证控件的行为
    }
    

    对控件的点击验证是调用performClick(),然后断言验证其行为。对于ListView这类涉及到Adapter的控件的点击验证,写法如下:

    //listView被展示之后

    listView.performItemClick(listView.getAdapter().getView(position, null, null), 0, 0);
    

    与button等控件稍有不同。

    六.Robolectric单元测试编写结构

    如下实例:

    
    

    未完待续......

    相关文章

      网友评论

        本文标题:Android单元测试

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