美文网首页
Android开发之逻辑单元测试

Android开发之逻辑单元测试

作者: 大大大大大先生 | 来源:发表于2017-11-15 16:28 被阅读38次

    单元测试的必要性

    • 完整,规范的单元测试有利于提升程序的“自动化”验证
    • 降低后期程序的维护成本
    • 高覆盖率的单元测试在很大程度上能提前发现一些潜在的bug
    • 编写单元测试的过程中可以帮助程序模块化重构,一个耦合性非常高的程序是无法针对他编写完善的单元测试的
    private void connectImpl(String domain, String ip, int port) {
            callOnStartConnect(domain,ip,port);
            try {
                socket = createSocket();
                String hostname = domain;
                if (!"".equals(ip) && ip != null) {
                    hostname = ip;
                }
                //String hostname = DomainManager.getHostName(domain, ip);
                long start = System.currentTimeMillis();
                InetSocketAddress inetSocketAddress = createInetSocketAddress(hostname, port);
                long end = System.currentTimeMillis();
                long inetTime = end - start;
                InetAddress inetAddress = inetSocketAddress.getAddress();
                if (inetAddress != null) {
                    ip = inetAddress.getHostAddress();
                }
                long startConn = System.currentTimeMillis();
                socket.connect(inetSocketAddress, connectTimeout);
                long endConn = System.currentTimeMillis();
                long connectTime = endConn - startConn;
                LogUtil.i(TAG, "connect时间间隔(ms):" + connectTime);
                inputStream = socket.getInputStream();
                outputStream = socket.getOutputStream();
                TCPConnection.this.domain = domain;
                TCPConnection.this.ip = ip;
                TCPConnection.this.port = port;
                callOnConnected(domain,ip,port,inetTime,connectTime);
    
                LogUtil.i(TAG, "connect to server success,domain:" + domain + ",port:" + port);
            } catch (IOException e) {
                LogUtil.w(TAG, e);//注意:Log工具直接把UnknownHostException返回""
    
                callOnConnectFailed(domain, ip, port,e);
                return;
            }
            startReadData();
        }
    
    private InetSocketAddress createInetSocketAddress(String hostname, int port) {
            long start = System.currentTimeMillis();
            InetSocketAddress inetSocketAddress = new InetSocketAddress(hostname, port);
            long end = System.currentTimeMillis();
            long inetTime = end - start;
            LogUtil.i(TAG, "inetSocketAddress时间间隔(ms):" + inetTime);
            InetAddress inetAddress = inetSocketAddress.getAddress();
            String localDnsIp = null;
            if (inetAddress != null) {
                localDnsIp = inetAddress.getHostAddress();
            }
            LogUtil.i(TAG, "local dns ip:" + localDnsIp);
            return inetSocketAddress;
        }
    

    以上createInetSocketAddress方法就是我在编写单元测试的时候单独抽离出来的方法,一方面我需要mock一个InetSocketAddress来满足测试需求,另一方面,单独抽离一个createInetSocketAddress方法从代码上看也是必要的,让方法职责更加单一,如果把createInetSocketAddress的实现直接耦合到connectImpl方法中,那么connectImpl的代码除了连接tcp的逻辑外还有创建InetSocketAddress的逻辑,这样就比较混乱,而且方法体也变长

    Android单元测试的分类

    纯代码逻辑的单元测试,也就是Java单元测试,在test目录下

    • 目前我们项目中用junit + powermock这一套单元测试框架,选择powermock的一个重要的原因就是:现如今比较流行的Mock工具如jMock 、EasyMock 、Mockito等都有一个共同的缺点:不能mock静态、final、私有方法等。而PowerMock能够完美的弥补以上三个Mock工具的不足,具体详细信息可以参考这篇文章:http://blog.csdn.net/jackiehff/article/details/14000779

    什么是mock?为什么要mock?

    • mock就是模拟,在代码逻辑测试中,有时候我们需要某一个方法返回我们指定的值,这样才能跑我们预测的代码逻辑,从而通过验证执行结果的正确性来反映该代码逻辑是否有问题,比如:
    /**
         * 开始处理同步通知任务
         *
         * @param syncKey 消息版本号
         */
        private synchronized void executeTask(final long imAccountId, final long syncKey) {
            Queue<Runnable> queue = getTaskQueue(imAccountId);
            queue.offer(new Runnable() {
                @Override
                public void run() {
                    handleInformResponse(imAccountId, syncKey, new OnNextCallback() {
                        @Override
                        public void onNext() {
                            LogUtil.d(TAG, "sync request on next");
                            scheduleNext(imAccountId);
                        }
                    });
                }
            });
            if (isLocked(imAccountId)) {
                LogUtil.w(TAG, "the imAccountId [" + imAccountId + "] is locked.");
                return;
            }
            lock(imAccountId);
            scheduleNext(imAccountId);
            LogUtil.d(TAG, "execute the inform task");
        }
    
    /**
         * 检测一个会话的同步通知处理是否被锁定
         *
         * @param imAccountId im帐户系统id
         * @return true表示已该帐户有正在处理中的同步通知,反之
         */
        private boolean isLocked(long imAccountId) {
            synchronized (imformLockMap) {
                LockStatus lockStatus = imformLockMap.get(imAccountId);
                if (lockStatus == null) {
                    return false;
                }
                if (System.currentTimeMillis() - lockStatus.lockTimestamp >= 20000 && lockStatus.locked) {
                    lockStatus.locked = false;
                    imformLockMap.put(imAccountId, lockStatus);
                }
                return lockStatus.locked;
            }
        }
    

    上面在测试executeTask方法的时候,isLocked返回true和false分别执行的是不通的分支逻辑,因此需要通过控制isLocked的返回值来分别覆盖到这两个逻辑执行流程,mock方法isLocked并返回指定的值,首先需要创建一个经过mock的对象,只有mock的对象才能mock对象中的所有方法或者变量:

    syncInformHandler = PowerMockito.mock(SyncInformHandler.class);
    PowerMockito.doReturn(false).when(syncInformHandler, "isLocked", Mockito.anyLong());
    // or
    PowerMockito.doReturn(true).when(syncInformHandler, "isLocked", Mockito.anyLong());
    
    • mock后的对象有什么区别?
    HeartConfig heartConfig = PowerMockito.mock(HeartConfig.class);
    // heartConfig = new HeartConfig();
    

    如上heartConfig被mock后生成的对象,它与new出来对象的区别在于,new出来的heartConfig对象,当你调用getMinHeart()方法的时候会真正的去执行这个方法,而且对象被new出来之后,对象中的一些值已经被初始化了,例如对象中的变量的赋值,静态代码块,构造函数都已经执行;但是对于mock出来的heartConfig对象,它的一切都是空的,调用getMinHeart()也不会真正的去执行这个方法,而是执行powermock框架的代理方法,heartConfig对象中的全局变量的复制都是空的,比如说:

    private int test = 2;
    

    如果对象new出来之后,那么test的值一定就是2,而对于mock出来的对象,test的值是0

    powermock几种常用的mock方式

    • 如果使用powermock,需要在类加入注释:
    @RunWith(PowerMockRunner.class)
    public class HeartStateContextTest {
    // ...
    }
    
    • 如果需要mock对象中的private,final,static,native方法或者final class,使用PowerMockito.whenNew,需要给类加入注解,注解里加入你要mock的class:
    @PrepareForTest({SyncInformHandler.class, ManagerFactory.class, RequestEntityFactory.class, AccountStore.class})
    public class SyncInformHandlerTest {
    // ...
    }
    
    • mock常规的public方法
    HeartConfig heartConfig = PowerMockito.mock(HeartConfig.class);
    PowerMockito.doReturn(120).when(heartConfig).getMinHeart();
    PowerMockito.doReturn(580).when(heartConfig).getMaxHeart();
    PowerMockito.doReturn(60).when(heartConfig).getStep();
    PowerMockito.doReturn(3).when(heartConfig).getMaxFailedCount();
    
    • mock类中的private方法
    syncInformHandler = PowerMockito.mock(SyncInformHandler.class);
    PowerMockito.doReturn(false).when(syncInformHandler, "canDoSync", Mockito.anyLong(), Mockito.anyLong());
    
    • mock类中的静态方法
    PowerMockito.mockStatic(ManagerFactory.class);
    managerFactory = PowerMockito.mock(ManagerFactory.class);
    PowerMockito.when(ManagerFactory.getInstance()).thenReturn(managerFactory);
    
    • doReturn和thenReturn的区别
    SyncInformHandler syncInformHandler = PowerMockito.mock(SyncInformHandler.class);
    PowerMockito.doReturn(1).when(syncInformHandler).getReturn();
    Assert.assertEquals(1, syncInformHandler.getReturn());
    PowerMockito.when(syncInformHandler.getReturn()).thenReturn(2);
    Assert.assertEquals(2, syncInformHandler.getReturn());
    

    以上的测试用例代码是可以正常跑通的,这里说明二者都可用

    PowerMockito.mockStatic(ManagerFactory.class);
    managerFactory = PowerMockito.mock(ManagerFactory.class);
    PowerMockito.when(ManagerFactory.getInstance()).thenReturn(managerFactory);
    // 不能用如下写法
    // PowerMockito.doReturn(managerFactory).when(ManagerFactory.getInstance());
    

    以上的代码就显示出doReturn和thenReturn的区别了,thenReturn之前的when里的参数是可以调用响应方法的,但是doReturn后面的when只能是一个Object类型的参数

    List list = new LinkedList();
    List spy = PowerMockito.spy(list);
    // 以下会抛出IndexOutOfBoundsException异常
    // PowerMockito.when(spy.get(0)).thenReturn("sss");
    PowerMockito.doReturn("sss").when(spy).get(0);
    Assert.assertEquals("sss", spy.get(0));
    

    以上代码,注释掉的不能用,会抛出IndexOutOfBoundsException异常,因为thenReturn会调用真实的方法执行,而doReturn不会,只会执行stubbed(插桩)方法

    • mock类中private变量
    IMInternal imInternal = PowerMockito.mock(IMInternal.class);
    PushInfo pushInfo = PowerMockito.mock(PushInfo.class);
    PowerMockito.doReturn(pushInfo).when(imInternal).getPushInfo();
    syncInformHandler = PowerMockito.mock(SyncInformHandler.class);
    Whitebox.setInternalState(syncInformHandler, "imInternal", imInternal);
    

    这里mock了SyncInformHandler类中的private类型的全局变量imInternal,其实就是通过Whitebox.setInternalState方法把我们外部生成的imInternal对象通过反射set进去

    • 对于一个mock对象,有时候我们并不需要里面所有的方法都被mock,有一些方法我们还是想真正执行,可用:doCallRealMethod()方法来标记某一个方法要被真的调用:
    PowerMockito.doCallRealMethod().when(syncInformHandler).handle(Mockito.any(PushRequest.class), Mockito.any(PushResponse.class));
    
    • 对于一个new出来的对象,我们想控制该对象中某些方法的返回值,由于不是mock对象,所以无法mock里面的方法,但是可以通过PowerMockito.spy()来监视这个real object
    List list = new LinkedList();
    List spy = PowerMockito.spy(list);
    PowerMockito.doReturn("sss").when(spy).get(0);
    Assert.assertEquals("sss", spy.get(0));
    

    这里有一点需要注意,Mockito.spy()和PowerMockito.spy()区别在于Mockito无法监视对象的final方法,但是PowerMockito可以,其实PowerMockito是基于Mockito的基础上拓展开发的,所以功能更加强大,也兼容了Mockito的功能

    • mock对象自定义的构造函数
    SyncInformHandler syncInformHandler = PowerMockito.mock(SyncInformHandler.class);
    

    如果是用以上方式去mock出来的对象,那么是通过默认空参数的构造函数去mock的,想通过自定义带参数的构造函数去mock可用如下方式:

    SyncInformHandler syncInformHandler = PowerMockito.mock(SyncInformHandler.class);
    PowerMockito.whenNew(SyncInformHandler.class).withArguments(Mockito.anyInt()).thenReturn(syncInformHandler);
    SyncInformHandler s1 = new SyncInformHandler(2);
    PowerMockito.doReturn(5).when(s1).getReturn();
    Assert.assertEquals(5, s1.getReturn());
    

    当使用new SyncInformHandler(2)这个构造函数来创建对象s1的时候,whenNew就会强行把s1替换成我们mock的对象syncInformHandler,然后就能够对s1对象使用各种mock方法了,为什么要这么玩?总感觉多次一举,直接使用mock对象不就好了?我认为,这里可能会更加灵活,mock对象无法指定构造函数,而whenNew可以针对性的指定哪些构造函数new出来的对象是可以使用mock的,哪些构造函数new出来的对象是无需mock的

    • 验证方法是否有被执行过,验证方法被调用的次数;有时候一个方法并没有返回值,所以没办法通过判断返回值的方式来验证结果是否是我们锁预期的,因此可通过检测方法中某一个子方法是否被调用过,被调用的次数来检测是否符合我们的预期:
    验证public方法是否被执行过
    Mockito.verify(syncKeyManager, Mockito.never()).putServerSyncKey(Mockito.anyLong(), Mockito.anyLong());
    
    // 验证private方法是否被执行过2次
    PowerMockito.verifyPrivate(syncInformHandler, Mockito.times(2)).invoke("dealSyncInform", Mockito.any(PushResponse.class));
    
    // 验证指定构造函数是否被执行过,这个要和whenNew结合使用
    SyncInformHandler mock = PowerMockito.mock(SyncInformHandler.class);
            PowerMockito.whenNew(SyncInformHandler.class).withArguments(Mockito.any(IMInternal.class)).thenReturn(mock);
    SyncInformHandler read = new SyncInformHandler(null);
            PowerMockito.verifyNew(SyncInformHandler.class).withArguments(Mockito.any(IMInternal.class));
    

    如何编写单元测试用例

    • 首先,要理清楚程序逻辑,罗列出程序所有重要的分支,一般方案设计的时候会画一个流程图,可以把流程图细化下,满足什么条件跑if分支,满足什么条件跑else分支
    • 针对每一条程序逻辑分支流程编写一个单元测试用例方法,如果该程序逻辑分支很简单,可以把几个逻辑分支合并成一个单元测试方法
    @RunWith(PowerMockRunner.class)
    @PrepareForTest({SyncInformHandler.class})
    public class ResponseDispatcherTest {
    
        public void testJUnit() {
            if (isPass()) {
                System.out.println("pass");
            } else {
                System.out.println("no pass");
            }
        }
    
        public boolean isPass() {
            return true;
        }
    
        @Test
        public void testStart() throws Exception {
            ResponseDispatcherTest responseDispatcherTest = PowerMockito.mock(ResponseDispatcherTest.class);
            PowerMockito.doCallRealMethod().when(responseDispatcherTest).testJUnit();
    
            PowerMockito.doReturn(false).when(responseDispatcherTest).isPass();
            responseDispatcherTest.testJUnit();
    
            PowerMockito.doReturn(true).when(responseDispatcherTest).isPass();
            responseDispatcherTest.testJUnit();
        }
    }
    

    testJUnit方法中有两条逻辑分支,那么我们就能控制isPass()返回值来分别执行到这两条逻辑分支,这里只是举一个简单的编写用例,先不用看方法命名规范性问题

    • 对于android程序来说,很多时候代码里面可能会有android的一些相关的类,接口等,但是在JUnit环境下,是没有这些环境的,例如说Context,任何运行到android类的地方都会直接崩溃,这时候需要把这些类或者方法mock掉,返回我们指定的值,Java单元测试的重点是测试Java代码的逻辑,具体的android相关的是不关注的,可以通过android单元测试来测试android的相关代码
    • 每一个单元测试方法都要写详细的注释,减少后面其他人来维护这个单元测试的成本
    • 单元测试用户针对类去写,一个类:className对应一个单元测试用例类:TestclassName,而且包名是一样的,这样在单元测试用例类中就能直接访问protected方法了
    • 单元测试的编写也可以有“模块测试”与“集成测试”的概念,比如说一个方法里面执行了6个子方法,我们可分别验证这6个子方法的正确性,然后再验证这6个子方法合起来跑的结果是否是正确的,也就是验证一个功能处理逻辑的正确性
    • 误区:场景测试,针对一些比较复杂场景的方案设计和编码,罗列出原先设计方案所支持的那些场景,然后用单元测试模拟这些场景来测试,例如我想测试一些多线程场景的问题,所以在测试方法里会开启多个线程,而且在这些线程运行结束之前这个测试方法不能结束,所以要wait,这样有可能导致这个测试方法运行很久,对于一个大工程来说,测试类可能有几十个,测试方法可能有上百个,那么会导致这个工程跑单元测试的时候跑很久才结束,可能会大大降低jinkens的编译速度,因为理论上一个单元测试方法的执行时间都是ms级别的,针对这种问题,考虑放到androidTest下去测试,不要在逻辑单元测试中做

    Android单元测试,在androidTest目录下

    • 四大组件的测试(生命周期)
    • UI测试
    • sqlite数据库测试
    • 文件存储测试(sd卡,SharedPreferences)

    相关文章

      网友评论

          本文标题:Android开发之逻辑单元测试

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