一.基本介绍
背景:
目前处于高速迭代开发中的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).变量没有赋值(即为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:语句覆盖)
- 各条错误处理通路测试:保证每一个异常都经过测试
如下是一个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");
}
}
运行结果:
四. 单元测试框架>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). Robolectric 是一个开源的framework,他们的做法是通过实现一套JVM能运行的Android代码,然后在unit test运行的时候去截取android相关的代码调用,然后转到他们的他们实现的代码去执行这个调用的过程。
举个例子说明一下,比如android里面有个类叫TextView
,他们实现了一个类叫ShadowTextView
。这个类基本上实现了TextView
的所有公共接口,假设你在unit test里面写到
String text = textView.getText().toString();
。在这个unit test运行的时候,Robolectric
会自动判断你调用了Android相关的代码textView.getText()
,然后这个调用过程在底层截取了,转到ShadowTextView
的getText
实现。而ShadowTextView
是真正实现了getText
这个方法的,所以这个过程便可以正常执行。
(2). 除了实现Android里面的类的现有接口,Robolectric还做了另外一件事情,极大地方便了unit testing的工作。那就是他们给每个Shadow类额外增加了很多接口,可以读取对应的Android类的一些状态。比如我们知道ImageView有一个方法叫setImageResource(resourceId),然而并没有一个对应的getter方法叫getImageResourceId(),这样你是没有办法测试这个ImageView是不是显示了你想要的image。而在Robolectric实现的对应的ShadowImageView里面,则提供了getImageResourceId()这个接口。你可以用来测试它是不是正确的显示了你想要的Image.环境配置
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时,默认就支持。
图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的常见用法。
- 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);
}
- 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单元测试编写结构
如下实例:
未完待续......