Android开发之逻辑单元测试

2017-11-15  本文已影响38人  大大大大大先生

单元测试的必要性

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目录下

什么是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());
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方式

@RunWith(PowerMockRunner.class)
public class HeartStateContextTest {
// ...
}
@PrepareForTest({SyncInformHandler.class, ManagerFactory.class, RequestEntityFactory.class, AccountStore.class})
public class SyncInformHandlerTest {
// ...
}
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();
syncInformHandler = PowerMockito.mock(SyncInformHandler.class);
PowerMockito.doReturn(false).when(syncInformHandler, "canDoSync", Mockito.anyLong(), Mockito.anyLong());
PowerMockito.mockStatic(ManagerFactory.class);
managerFactory = PowerMockito.mock(ManagerFactory.class);
PowerMockito.when(ManagerFactory.getInstance()).thenReturn(managerFactory);
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(插桩)方法

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进去

PowerMockito.doCallRealMethod().when(syncInformHandler).handle(Mockito.any(PushRequest.class), Mockito.any(PushResponse.class));
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的功能

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));

如何编写单元测试用例

@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单元测试,在androidTest目录下

上一篇 下一篇

猜你喜欢

热点阅读