Android单元测试研究与实践

发表于:2016-3-30 11:27

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:潘铭炜 黄超    来源:51Testing软件测试网采编

  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单元测试编写流程
  单元测试最终需要输出文档式的单元测试代码,为线上代码提供良好的代码稳定性保证。
32/3<123>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号