Android单元测试研究与实践
作者:网络转载 发布时间:[ 2016/3/30 13:59:21 ] 推荐标签:软件测试 单元测试
Robolectric使用介绍
Robolectric单元测试编写结构
单元测试代码写在项目的test(也可能是androidTest,该目录在项目中会呈浅绿色)目录下。单元测试也是一个标准的Java工程,以类为文件单位编写,执行的小单位是函数,测试用例(以下简称case)是带有@Test注解的函数,单元测试里面带有case的类由Robolectric框架执行,需要为该类添加注解@RunWith(RobolectricTestRunner.class)。基于Robolectric的代码结构如下:
//省略一堆import
@RunWith(RobolectricTestRunner.class)
public class MainActivityTest {
@Before
public void setUp() {
//执行初始化的操作
}
@Test
public void testCase() {
//执行各种测试逻辑判断
}
}
上述结构中,带有@Before注解的函数在该类实例化后,会立即执行,通常用于执行一些初始化的操作,比如构造网络请求和构造Activity。带有@test注解的是单元测试的case,由Robolectric执行,这些case本身也是函数,可以在其他函数中调用,因此,case也是可以复用的。每个case都是独立的,case不会互相影响,即便是相互调用也不会存在多线程干扰的问题。
常见Robolectric用法
Robolectric支持单元测试范围从Activity的跳转、Activity展示View(包括菜单)和Fragment到View的点击触摸以及事件响应,同时Robolectric也能测试Toast和Dialog。对于需要网络请求数据的测试,Robolectric可以模拟网络请求的response。对于一些Robolectric不能测试的对象,比如ConcurrentTask,可以通过自定义Shadow的方式现实测试。下面将着重介绍Robolectric的常见用法。
Robolectric 2.4模拟网络请求
由于商业App的多数Activity界面数据都是通过网络请求获取,因为网络请求是大多数App首要处理的模块,测试依赖网络数据的Activity时,可以在@Before标记的函数中准备网络数据,进行网络请求的模拟。准备网络请求的代码如下:
public void prepareHttpResponse(String filePath) throws IOException {
String netData = FileUtils.readFileToString(FileUtils.
toFile(getClass().getResource(filePath)), HTTP.UTF_8);
Robolectric.setDefaultHttpResponse(200, netData);
}//代码适用于Robolectric 2.4,3.0需要注意网络请求的包的位置
由于Robolectric 2.4并不会发送网络请求,因此需要本地创建网络请求所返回的数据,上述函数的filePath便是本地数据的文件的路径,setDefaultHttpResponse()则创建了该请求的Response。上述函数执行后,单元测试工程便拥有了与本地数据数据对应的网络请求,在这个函数执行后展示的Activity便是有数据的Activity。
在Robolectric 3.0环境下,单元测试可以发真的请求,并且能够请求到数据,本文依旧建议采用mock的办法构造网络请求,而不要依赖网络环境。
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 testActivityTurn(ActionBarActivity firstActivity, Class secondActivity) {
Intent intent = new Intent(firstActivity.getApplicationContext(), secondActivity);
assertEquals(intent, Robolectric.shadowOf(firstActivity).getNextStartedActivity());//3.0的API与2.4不同
}
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等控件稍有不同。
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是否弹出。
Shadow写法介绍
Robolectric的本质是在Java运行环境下,采用Shadow的方式对Android中的组件进行模拟测试,从而实现Android单元测试。对于一些Robolectirc暂不支持的组件,可以采用自定义Shadow的方式扩展Robolectric的功能。
@Implements(Point.class)
public class ShadowPoint {
@RealObject private Point realPoint;
...
public void __constructor__(int x, int y) {
realPoint.x = x;
realPoint.y = y;
}
}//样例来源于Robolectric官网
上述实例中,@Implements是声明Shadow的对象,@RealObject是获取一个Android 对象,constructor则是该Shadow的构造函数,Shadow还可以修改一些函数的功能,只需要在重载该函数的时候添加@Implementation,这种方式可以有效扩展Robolectric的功能。
Shadow是通过对真实的Android对象进行函数重载、初始化等方式对Android对象进行扩展,Shadow出来的对象的功能接近Android对象,可以看成是对Android对象一种修复。自定义的Shadow需要在config中声明,声明写法是@Config(shadows=ShadowPoint.class)。
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框架的网络依赖解除如下:
//代码参考了参考文献[3]
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可以由单元测试编写者设置,而不来源于网络,从而解除了对网络环境的依赖。
在实际项目中使用Robolectric构建单元测试
单元测试的范围
在Android项目中,单元测试的对象是组件状态、控件行为、界面元素和自定义函数。本文并不推荐对每个函数进行一对一的测试,像onStart()、onDestroy()这些周期函数并不需要全部覆盖到。商业项目多采用Scrum模式,要求快速迭代,有时候未必有较多的时间写单元测试,不再要求逐个函数写单元测试。
本文单元测试的case多来源于一个简短的业务逻辑,单元测试case需要对这段业务逻辑进行验证。在验证的过程中,开发人员可以深度了解业务流程,同时新人来了看一下项目单元测试知道哪个逻辑跑了多少函数,需要注意哪些边界——是的,单元测试需要像文档一样具备业务指导能力。
在大型项目中,遇到需要改动基类中代码的需求时,往往不能准确快速地知道改动后的影响范围,紧急时多采用创建子类覆盖父类函数的办法,但这不是长久之计,在足够覆盖率的单元测试支持下,跑一下单元测试知道某个函数改动后的影响,可以放心地修改基类。
美团的Android单元测试编写流程如图4所示。
美团Android单元测试实施结构
图4 美团Android单元测试编写流程
单元测试终需要输出文档式的单元测试代码,为线上代码提供良好的代码稳定性保证。
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系SPASVO小编(021-61079698-8054),我们将立即处理,马上删除。
相关推荐
iOS单元测试mocha、chai、sinon和istanbul实现百分之百的单元测试覆盖率关于单元测试的总结及思考编写更好的Java单元测试的7个技巧Android单元测试框架Robolectric3.0介绍(一)使用Kiwi单元测试总结单元测试如此重要,为什么你不知道Python单元测试??使用装饰器实现测试跳过和预期故障对Controller的单元测试写好单元测试的10个技巧单元测试的重要性Angular单元测试系列??Component、Directive、Pipe 以及ServiceAndroid单元测试的整理提升单元测试体验的利器--Mockito使用总结iOS UnitTest单元测试Vue的单元测试探索(二)
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11热门文章
常见的移动App Bug??崩溃的测试用例设计如何用Jmeter做压力测试QC使用说明APP压力测试入门教程移动app测试中的主要问题jenkins+testng+ant+webdriver持续集成测试使用JMeter进行HTTP负载测试Selenium 2.0 WebDriver 使用指南