mockitoandroid_Android使用Mockito和Roboletric进行单元测试

好的测试用例常常能让开发效率和质量大大提升,但是代码设计有时候会使测试用例无从下手、难以书写,神烦,很多时候会让开发者忽略做单元测试,又或干脆就懒得写了。在开源界也涌出了很多优秀的单元测试框架,就是为了弥补“单元测试不好写”这个缺陷:Mockito 强大的模拟工具,能够模拟出无关的依赖模块、行为,并能够验证调用顺序;

Roboletric 在JVM上模拟Android测试环境(即通过单元测试就能测试部分Android代码)。

本文结合这两个库来介绍一下在Android开发中如何写单元测试。

为什么要单元测试?

在写单元测试的时候,你需要想明白这个问题,才能更有针对性地去写测试用例。

单元测试的好处体现在于:你做了一个很大的底层改动,跑一遍单元测试,哇,全通过了!测试通过让你有信心将代码发布出去;

在写测试的时候你会对自己的代码进行思考,每一个操作应用后将会产生什么样的期望后果;

在快速开发时,写几个单元测试保证代码质量。

就拿Fresco里面的单元测试举个例子,它测试DraweeView在attach/detach到Window上时的反应:1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16@Test

public void testLifecycle_Controller()   {

InOrder inOrder = inOrder(mController); // 初始化mockito类,验证调用顺序

mDraweeView.setHierarchy(mDraweeHierarchy);

mDraweeView.setController(mController);

inOrder.verify(mController).setHierarchy(mDraweeHierarchy); // 是否调用了setHierarchy

mDraweeView.onAttachedToWindow(); // 模拟View attachWindow   事件

inOrder.verify(mController).onAttach(); // 是否调用了 onAttach

mDraweeView.onStartTemporaryDetach(); // 模拟View temporaryDetach 事件

inOrder.verify(mController).onDetach(); // 是否调用了 onDetach

mDraweeView.onFinishTemporaryDetach(); // 模拟View finishTemporaryDetach 事件

inOrder.verify(mController).onAttach(); // 是否调用了 onAttach (结束临时detach,即应该重新attach)

mDraweeView.onDetachedFromWindow(); // 模拟View detach 事件

inOrder.verify(mController).onDetach();  // 是否调用了 onDetach

}

上面这段测试用例很简单,但是功能很强大,它保证了这个DraweeView在View的attach/detach处理这个环节的代码正确。

StackOverflow上有个回答很棒,你只有开始写单元测试后才能体会到它的强大,那么我们来学习一下吧。

Mockito的作用

通常组件之间都会有模块化依赖,但是写单元测试时一般仅涉及一两个组件,若要复现操作环境是非常困难的事情。而Mockito能够轻易Mock一个依赖模块实例,并指定它的行为,把精力用在你想要的那一小个单元的测试。

1. Mock模拟行为

这是它的精髓之一,可以通过注解@Mock(语法糖)或者Mockito.mock(Class clazz)模拟出一个实例,传入的不论是Interface, Abstract class还是普通Class,统统都会mock出一个继承原类、填满hook的新类。注意这个生成的新类是一个空壳,如果需要使用必须指定行为(Stub)。对于未Stub的方法,通常返回null。我们可以写个test来验证一下:1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26class ExampleClass {

public String test() {

return "test";

}

}

//test

class ExambleClassTest {

@Mock ExampleClass mTestClass;

@Before

public void setUp() {

MockitoAnnotations.initMocks(this); // 初始化被@Mock注解的类

}

@Test

public void testOrigin() {

Assert.assertEquals("test", mTestClass.test()); // Fail

}

@Test

public void testMock() {

Mockito.when(mTestClass.test()).thenReturn("testMock");

Assert.assertEquals("testMock", mTestClass.test()); //   Success

}

}

上面这段测试用例中,testOrigin()会报失败,因为test()方法被hook后返回了null。testMock()会成功,因为我们把这个方法Stub住了,让它返回了”testMock”。

除了mock,还有另一种用法:@Spy/spy(Class clazz)。它可以对一个类的实例(或者含有无参构造函数的类)进行模拟,未指定行为的方法由原类处理,指定行为的类由hook处理。对上面的ExampleClass,我们可以用Spy来进行一个测试:1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19public class ExampleClassTest {

@Spy ExampleClass mExampleClass;

@Before

public void setUp() {

MockitoAnnotations.initMocks(this);

}

@Test

public void testOrigin() { // Success

Assert.assertEquals("test", mExampleClass.test());

}

@Test

public void testMockSpy() { // Success

Mockito.doReturn("mockSpy").when(mExampleClass).test();

Assert.assertEquals("mockSpy", mExampleClass.test());

}

}

这里testOrigin()和testMockSpy()都会通过(注意在@Mock注释下testOrigin()会失败),因为Spy只是部分Mock,对没有模拟行为的部分仍返回原结果,但是用起来也会不一样,具体看下文。

模拟行为(Stub)一共有两类做法:Mockito.when(obj.methodCall()).thenReturn(result); 会检查返回类型;不可用于重复Stub、返回void的函数、Spy作用下mock类的call。

Mockito.doReturn(result).when(obj).methodCall(); 不会检查返回类型,可重复Stub

之所以说是两类做法,是因为里面的所有return都可以换成answer,不仅指定返回,还可以指定methodCall中做的事情:1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29class CountClass {

int count = 0;

public void addCount() {

count++;

}

}

// 测试用例

class CountClassTest {

@Mock CountClass mTestClass;

@Before

public void setUp() {

MockitoAnnotations.initMocks(this);

Mockito.doAnswer(new Answer {

public Void answer(InvocationOnMock   invocation) throws Throwable {

mTestClass.count += 2;

return null;

}

}).when(mTestClass).addCount();

}

@Test

public void testMockVoid() { // Success

mTestClass.addCount();

Assert.assertEquals(2, mTestClass.count);

}

}

ps: 如上调用是唯一一种Stub void返回函数的做法。

2.验证行为

由于Mockito在mock出来的对象中四处都是hook,所以它可以做到一个很棒的功能:验证调用。基于上文的CountClass你可以写一个如下的简单测试:1

2

3

4

5

6

7@Test

public void testAddCount() throws   Exception {

mCountClass.addCount();

mCountClass.addCount();

Mockito.verify(mCountClass, new Times(2)).addCount();

Assert.assertEquals(4, mCountClass.count);

}

3.题外话

when(obj.methodCall()).thenReturn(something)时这个方法到底有没有被执行呢?我感到很好奇。于是我试验了一下,发现它确实是被调用了,但是是无法用Mockito.verify验证到的。你可以跑一下如下测试:1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33class TestClass {

int count = 0;

public String test() {

return "test";

}

}

class TestClassTest {

@Mock TestClass mTestClass;

@Before

public void setUp() {

MockitoAnnotations.initMocks(this);

}

@Test

public void testAddCount() {

Mockito.when(mTestClass.addCount()).thenAnswer(ew Answer   {

public String answer(InvocationOnMock   invocation) throws Throwable {

mTestClass.count += 2;

return "test";

}

});

Mockito.when(mTestClass.addCount()).thenAnswer(ew Answer   {  //此处会调用一次第一次的Stub

public String answer(InvocationOnMock   invocation) throws Throwable {

mTestClass.count += 2;

return "test";

}

});

Assert.assertEquals(2, mCountClass.count); // Success

Mockito.verify(mTestClass).addCount(); // Fail

}

}

Mockito.when(mTestClass.addCount()).thenAnswer() 这里一共有三步: mTestClass.addCount() -> when() -> thenAnswer()。

由于mTestClass.addCount()确实被调用了,所以它产生的后果是持久性的,所以后续验证mCountClass.count==2是对的。但是这里又蛮有意思的,为什么不会被verify?我去翻阅了一下源码,发现了两处相关代码,供大家理解:

3.1 MockHandlerImpl中处理函数调用:

这里就是所有的mock对象hook处理集中处,它是类似动态代理的处理。1

2

3

4

5

6

7

8

9

10public Object handle(Invocation   invocation) throws Throwable {

// ...

// 将此次调用设置为可能的Stub对象

invocationContainerImpl.setInvocationForPotentialStubbing(invocationMatcher);

OngoingStubbingImpl ongoingStubbing = new OngoingStubbingImpl(invocationContainerImpl);

mockingProgress.reportOngoingStubbing(ongoingStubbing);

// 寻找是否有Stub的方法调用,有则调用,否则返回默认返回

}

在setInvocationForPotentialStubbing(invocation)中它将此次调用添加到了一个LinkedList中。

3.2 OugoingStubbingImpl中添加Stub1

2

3

4

5

6

7

8

9

10

11

12

13

14// OutgoingStubbingImpl.java

public OngoingStubbing thenAnswer(Answer>   answer) {

// 如果没有可Stub的对象则报错

invocationContainerImpl.addAnswer(answer); // 将这个Answer设置为Stub返回,并且移除上一个调用记录(肯定是被Stub的函数调用)。

return new ConsecutiveStubbing(invocationContainerImpl);

}

// InvocationContainerImpl.java

public void addAnswer(Answer   answer) {

registeredInvocations.removeLast();

addAnswer(answer, false);

}

这里将调用记录,也就是后续verify的时候检查的东西抹去了!

所以说Mocktio.when(obj.methodCall()).thenAnswer(answer)在重复Stub的时候是存在问题的,虽然不会被verify到,但如果Stub的Answer中做了一些持久改变,它会在下次被Stub时的那次调用中生效。多次Stub可以用Mocktio.doReturn(something).when(obj).methodCall(),这里它在真实的调用之前已经将Stub替换掉了,所以不会出现这个问题。

Roboletric能做的事

Roboletric是另一个优秀的库,它的目标是你能够用单元测试来测试一些Android相关的代码。正常情况下跟Activity、Service有关的测试需要走Instrument测试,在实机上跑,然而Robolectric可以让你轻松地在电脑上就跑起Android测试。

Robolectric使用了自己的ClassLoader,它在UnitTest运行时插入了各类Shadow Object来为Android原生类添加一些hook用于测试。RobolectricTestRunner会从它上传的org.robolectric:android-all里面去拿对应的Android SDK。这个原理比较复杂,先不深究了,主要看看用法吧:

1.基础用法

首先你有一个MainActivity,它含有R.id.root_view的一个根View,里面有一个Button点击可以跳转SecondActivity。你可以通过以下这段代码来让你的Activity走一遍onCreate->onStart->onResume,并做一些检查:1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32@RunWith(RobolectricGradleTestRunner.class)

@Config(constants = BuildConfig.class,   sdk = 21)

public class MainActivityTest {

ActivityController mActivityController;

@Before

public void setup(){

mActivityController =   Robolectric.buildActivity(MainActivity.class).create().start().resume().visible();

}

@Test

public void testBase() {

Assert.assertTrue(Shadows.shadowOf(mActivityController.get()).isTaskRoot());   //是否MainActivity在Task root

ViewGroup view = (ViewGroup)   mActivityController.get().findViewById(R.id.root_view);

Assert.assertNotNull(view);                     // 是否存在view

ShadowView shadow = Shadows.shadowOf(view);

Assert.assertTrue(shadow.isAttachedToWindow()); // 是否已经attachToWindow

TextView textView = new TextView(mActivityController.get());

view.addView(textView);

Assert.assertTrue(shadow.didRequestLayout());   // 是否调用了requestLayout

}

@Test

public void testButton() {

Button btn = (Button)   mActivityController.get().findViewById(R.id.button);

Assert.assertNotNull(btn);

btn.performClick();

ShadowActivity shadowActivity = Shadows.shadowOf(mActivityController.get());

Intent intent = new Intent(mActivityController.get(),   SecondActivity.class);

Assert.assertEquals(shadowActivity.getNextStartedActivity().getComponent(),   intent.getComponent());

}

}

这里有几点注意:使用@RunWith(RobolectricGradleTestRunner.class)时需要指定@Config(constants = BuildConfig.class),它会从/build/intermediates/目录下找到merge的Manifest、Resource、Asset目录并加载。如果使用@RunWith(RobolectricTestRunner.class)则需要手动指定@Config(manifest = "...", resourceDir =      "...", assetDir = "..."),manifest设置的目录base于Unit Test Config里面的”Working      Directory”(见下图)

其他值resourceDir、assetDir的目录Base于manifest的父目录。visible() 可能会令你感到困惑,因为它不属于Activity生命周期之一,但是执行visibile()会保证Activity的到Window上(包括初始化DecorView      + add到WindowManager),否则findViewById()、attachToWindow等都会有问题。

关于MultiDex

Robolectric是在JVM上运行代码,根本没有”MultiDex”这回事,如果你的项目里用到了它,需要额外加入一个依赖:

testCompile "org.robolectric:shadows-multidex:${robolectricVersion}"

它hook了MultiDex这个类,让它在install的时候啥也不干。

2.进阶使用:各类Shadow

Shadow是Robolectric里面的Hook类前缀,你可以利用Shadow得到一些Android组件无法获取到的状态,因为有的属性没有变量维护/没有public getter,又或无法获取上次操作结果,这时候就可以使用Shadow来定制一些需要的属性保存,看下面这个例子:

你的Activity里面有一个View(R.id.view), 它将OnClickListener设置为了Activity自身,在点击时会发一个广播(action=”com.desmond.androidtest.TestBroadcast”),我们想验证这个情况。但是有两个问题:View无法获取到OnClickListener,只能知道hasOnClickListener();

Receiver不是你定义的,你只负责发,你如果需要验证需要自己额外注册一个Receiver,并编译运行检查是否成功。

使用Robolectric,你可以写下这么一个测试:1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30@RunWith(RobolectricGradleTestRunner.class)

@Config(constants = BuildConfig.class,   sdk = 21)

public class MainActivityTest {

ActivityController mActivityController;

@Before

public void setup(){

mActivityController =   Robolectric.buildActivity(MainActivity.class).create().start().resume().visible();

}

@Test

public void testBase() {

View view =   mActivityController.get().findViewById(R.id.view);

Assert.assertNotNull(view);                     // 是否存在view

ShadowView shadow = Shadows.shadowOf(view);

Assert.assertEquals(shadow.getOnClickListener(),   mActivityController.get()); // 是否已经attachToWindow

view.performClick();

ShadowActivity shadowActivity =   Shadows.shadowOf(mActivityController.get());

Intent expectedIntent = new Intent("com.desmond.androidtest.TestBroadcast");

List intentList =   shadowActivity.getBroadcastIntents();  //   获取发出的Broadcast

boolean hasIntentSend = false;

for (Intent i : intentList) {

if(i.getAction().equals(expectedIntent.getAction()))   {

hasIntentSend = true;

break;

}

}

Assert.assertTrue(hasIntentSend);    // 是否发出指定Broadcast

}

}

很有意思吧,更多的”Shadow”可以在Shadows这个类里面找着,用的时候直接Shadows.shadowOf(object)就可以返回对应Shadow实例。你还可以自定义Shadow来完成你需要的测试功能点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值