起因

我最近在自己实现一个图片加载的框架。由于框架比较大,一时半会儿完成不了,但是又不能等所有模块全写好了再一起验证,所以很自然的就想到了测试。

android的两种测试框架

一般来说,android有两种测试方案。一种是把测试代码放在android机上去运行,叫做Instrumented Test。而另一种则是在本地Java环境上跑,使用JUnit+Mock框架,就叫做Local Unit Test吧。

Andorid项目中自然有大量使用android.jar的代码,把测试代码放在android机上跑,当然是没有任何问题的。而在本机(单纯的java环境)跑就出问题了,因为没有运行着的android环境,所以一遇到需要android.jar的指令,就没办法了。

想要在本地测试android,就需要使用Mock框架

Mock(即伪造,模仿),Mock框架能够伪造出任何的类,并且定义这个伪造出来的类的行为。自然也可以对android代码的行为进行伪造。

二者对比,androidTest比较自由一些,因为是放在android机上测试,所以对代码不需要进行任何修改。但是因为要把测试上传到Android机上运行,相对较慢,同时文件系统等也不容易配置。

而后者恰恰相反,会快很多,显得轻量许多。缺点就是需要考虑伪造所有用到的android的类。

在我的这次项目中,用到的android代码不算很多,所以选择后者。并对其进行了大概的了解。

JUnit与Mockito

JUnit4

Junit是一套针对Java,通用的测试框架。不仅仅局限于Android,使用非常简单。

1
2
3
4
@Test
public void newArrayListsHaveNoElements() {
assertThat(new ArrayList().size(), is(0));
}

在测试类中用@Test注解方法,运行JUnit,它就会自动执行这个方法,如有问题,抛出异常。

当然异常可能是故意让它抛出的,这也是需要测试的,@Test(expected=NullPointerException.class)可以断言函数会抛出NullPointerException异常。

另外,@Test(timeout=200)可以断言函数会在200ms内结束

Assert类是一个工具类,提供许多静态的工具函数,来使用断言。(所谓断言,断言一个语句为真,那么为假则报错)。

除了@Test外,还有其它注解。

@Before 在每一个test执行之前都会调用,@BeforeClass 在类加载之时调用,初始化静态域,这个注解只能加在static函数上。@After @AfterClass同理。

@Ignore暂时忽略这个测试,在测试驱动开发中,一般是先写测试用例再完成代码的。那么就需要指明先忽略某些测试。

@Rule可以定义在每次Test运行之前,做一些工作。

1
2
3
4
5
6
7
8
public class MyTest {

@Rule
public MethodRule screenshot = new ScreenshotOnFailureRule();

@Test
public void myTest() { ... }
}

详见:Dale的博客 以及 Gist

Mockito

什么是Mockito?官网是这样说的。

The Mockito library enables mock creation, verification and stubbing.

Mockito作为一套Mock框架,能够伪造类的创建,验证(断言此类进行了xxx操作),以及定义伪造类的行为。(注:这一系列伪造的东西统称为Test doubles)

显然在测试android项目时,有关android的代码,就需要由它来伪造了。

其feature之多有30余条,可以在后面的参考中详查,这里只说一些一些基本概念,以及常用feature。

一些基本概念

Test double object (Mock object),可以分为以下几类:

  • Dummy 一个空对象,完全是为了满足compiler,满足一些method的参数。

  • Fake 一个模拟的对象,与Dummy不同的是,为这个对象定义了一些方法。但这个方法有水分,只要满足测试所需就可以了(比如一个 in-memory database)
    和Stub区别不明显,所以也常称为Stub

  • Stub 与Fake类似,给伪造类定义了方法。但是这个就没有水分了,很多情况需要写出实际代码。

  • Mock 是一种笼统的称呼,即模拟。一般来说,我们说Mock一个对象。那么这个对象a)有伪造出来的行为,b)能够verify这个对象进行了哪些操作,比如verify这个类的某个方法被调用了两次。

  • Spy 有时候我们已经有了实际的对象,不需要伪造。但是又想利用Mock的特性,这时候就需要用到Spy。Spy相当于实际对象的一个代理,其中某些方法可以被Stub,能够记录下来该类操作的信息,进而进行验证(verify)。

使用

1.Creating Mock

1
2
3
4
Flower flowerMock=Mockito.mock(Flower.class);
//---------Or----------
@Mock
Flower flowerMock;

这样就可以Mock一个对象。

2.Stubing Method

1
2
Flower flowerMock = mock(Flower.class);
when(flowerMock.getNumberOfLeafs()).thenReturn(TEST_NUMBER_OF_LEAFS);

这个样就定义了实例的行为。

这种then方法,还有 thenReturn(T) thenThrow(Throwable | Class<? extends Throwable>) then(Answer) thenCallRealMethod()

2.1.数据匹配

1
when(plantSearcherMock.smellyMethod(anyInt(), contains("asparag"), eq("red"))).willReturn(true);

有的时候,我们需要通过匹配符Matcher来定义行为。

2.2.返回多值

1
2
3
4
5
6
7
8
@Test
public void shouldReturnLastDefinedValueConsistently() {
WaterSource waterSource = mock(WaterSource.class);
when(waterSource.getWaterPressure()).thenReturn(3, 5);
assertEquals(waterSource.getWaterPressure(), 3);
assertEquals(waterSource.getWaterPressure(), 5);
assertEquals(waterSource.getWaterPressure(), 5);
}

当我们定义了多个返回值时,调用这个函数就会按照顺序返回。比如上例,第一次返回3,第二次返回5,以后都返回5

3.确认操作
一旦Mock了一个实例,这个实例会保存对它所有的操作。所以我们就可以对这些操作进行验证verify。

1
2
3
WaterSourcewaterSourceMock=mock(WaterSource.class);
waterSourceMock.doSelfCheck();
verify(waterSourceMock).doSelfCheck();

确认调用了doSelfCheck()方法,甚至可以通过verify的参数,传入特殊参数,进行更精细的操作,这些参数包括,times(int) never() atLeastOnce

除此之外,验证,还可以验证几个不同实例的函数调用的顺序。验证执行耗时。

4.最后:局限
Mockito在final class, static method等方面无能为力,所以就有人维护了开源库来解决这种情况。比如PowerMock等,会在后面介绍到。

参考:
http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html
https://dzone.com/refcardz/mockito

在AndroidStudio实践

配置Gradle

Gradle的插件会自动读取src/test/java下的文件,然后执行它。

1
2
3
4
dependencies {
testCompile 'junit:junit:4.12'
testCompile "org.mockito:mockito-core:1.9.5"
}

配置AndroidStudio

  1. 调整build.gradle改变android gradle plugin version,大于1.1.0-rc1(直接改build.gradle或者进入File > Project Structure)

  2. 把junit以及其它依赖加入build.gradle

  3. 打开JUnit,在Build variants中控制Instrumented test与unit tests

  4. 创建src/test/java文件夹,写test,并且右键运行。

用Gradble控制Test的运行

通过一行命令即可,./gradlew test --continue

这种运行可以通过在build.gradle中定义一些参数,另外,android中这一unit test系统,还对不同的Flavor或者build type能单独定义test。这些功能不一一介绍。在下面的链接中可以看到。

最后还需要提一个问题,如果你不想伪造android的类,同时也不想让android一运行就报错。(本地开发时用的android.jar不是真正的android sdk,很多方法都只有一行throw new Exception(“Not Stub!”);)

如果你想掩盖这个报错,只需要在build.gradble中设置

1
2
3
testOptions {
unitTests.returnDefaultValues = true
}

参考
http://tools.android.com/tech-docs/unit-testing-support

更多的测试框架

知道一些常用的开源库是干什么的,是很有必要的。这可以让你进行灵活的技术选型,同时,因为有很多测试框架都是有依赖的,熟悉它们,也可以让你快速的了解一个测试框架,分清它们的派系。

EasyMock vs Mockito

EasyMock是开派祖师,Mockito最开始是fork EasyMock开始写的,最终重写了很多逻辑,引入了一些JMock的特性,最终自成一家,青出于蓝。

PowerMock

可以和Mockito一起用,是Mockito的补充,针对Mockito中不能对final enum static方法进行伪造的问题,进行了解决。gradle比较啰嗦,记录如下:

1
2
3
4
testCompile 'org.powermock:powermock-module-junit4:1.6.4'
testCompile 'org.powermock:powermock-module-junit4-rule:1.6.4'
testCompile 'org.powermock:powermock-module-junit4-rule-agent:1.6.4'
testCompile 'org.powermock:powermock-api-mockito:1.6.4'

Robolectric

Robolectric伪造了整个Android SDK,甚至包含了很多native代码,使得Android代码可以在不运转整个android系统的情况下,运转android代码。
相对于JUnit在Android特有的Resource Assets,以及界面等机制,能够达到很好的测试效果。

相对而言,Robolectric有点接近黑盒测试。自然,它也可以和Mock framework共用。

下面是一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(RobolectricTestRunner.class)
public class MyActivityTest {

@Test
public void clickingButton_shouldChangeResultsViewText() throws Exception {
MyActivity activity = Robolectric.setupActivity(MyActivity.class);

Button button = (Button) activity.findViewById(R.id.button);
TextView results = (TextView) activity.findViewById(R.id.results);

button.performClick();
assertThat(results.getText().toString()).isEqualTo("Robolectric Rocks!");
}
}

Robolectric官网

Espresso

Espresso是一种典型的Instrumented Test。在View的测试方面较为擅长。

1
2
3
4
5
6
7
8
9
@Test
public void greeterSaysHello() {
onView(withId(R.id.name_field))
.perform(typeText("Steve"));
onView(withId(R.id.greet_button))
.perform(click());
onView(withText("Hello Steve!"))
.check(matches(isDisplayed()));
}

Espresso

测试的理论支持

待补充