Android单元测试框架Robolectric3.0介绍(2)

发表于:2016-6-08 13:44

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

 作者:geniusmart    来源:51Testing软件测试网采编

  一、闲话单元测试
  我们经常讲“前人种树,后人乘凉”,然而在软件开发中,往往呈现出来的却是截然相反的景象,我们在绩效和指标的驱使下,主动或被动的留下来大量坏味道的代码,在短时间内顺利的完成项目,此后却花了数倍于开发的时间来维护此项目,可谓“前人砍树,后人遭殃”,讽刺的是,砍树的人往往因为优秀的绩效,此时已经步步高升,而遭殃的往往是意气风发,步入职场的年轻人,如此不断轮回。所以,为了打破轮回,从一点一滴做起吧,“树”的种类众多,作为任意一名普通的软件工程师,种好单元测试这棵树,便是撒下一片荫凉。
  关于单元测试,很多人心中会有以下几个疑问:
  (1)为什么要写?
  (2)这不是QA人员该做的吗?
  (3)需求天天变,功能都来不及完成了,还要同时维护代码和UT,四不四傻啊?
  (4)我要怎么写UT(特别是Android单元测试)?
  关于第一个问题,首先我们反问自己几个问题:
  (1)我们在学习任何一个技术框架,比如 retofit2 、 Dagger2 时,是不是第一时间先打开官方文档(或者任意文档),然后查阅api如何调用的代码,而官方文档往往都会在最醒目的地方,用最简洁的代码向我们说明了api如何使用?
  其实,当我们在写单元测试时,为了测试某个功能或某个api,首先得调用相关的代码,因此我们留下来的便是一段如何调用的代码。这些代码的价值在于为以后接手维护/重构/优化功能的人,留下一份程序猿最愿意去阅读的文档。
  (2)当你写单元测试的时候,是不是发现很多代码无法测试?撇开对UT测试框架不熟悉的因素之外,是不是因为你的代码里一个方法做了太多事情,或者代码的封装性不够好,或者一个方法需要有其他很多依赖才能测试(高耦合),而此时,为了让你的代码可测试,你是不是会主动去优化一下代码?
  (3)是不是对重构没信心?这个话题太老生常谈了,配备有价值的、高覆盖率的单元测试可解决此问题。
  (4)当你在写Android代码(比如网络请求和DB操作)的时候,是如何测试的?跑起来整个App,点了好几步操作后,终于到达要测试的功能,然后巨慢无比的Debug?如果你写UT,并使用Robolectric这样的框架,你不仅可以脱离Android环境对代码进行调试,还可以很快速的定位和Debug你想要调试的代码,大大的提升了开发效率。
  以上,便是写好单元测试的意义。
  关于第二个问题,己所不欲勿施于人
  我始终觉得让QA写UT,是一种傻叉的行为。单元测试是一种白盒测试,本来就是开发分内之事,难道让QA去阅读你恶心的充满坏味道的代码,然后硬着头皮写出UT?试想一下,你的产品经理让你画原型写需求文档,你的领导让你去市场部辅助吹嘘产品,促进销售,你会不会有种吃了翔味巧克力的感觉?所以,己所不欲勿施于人。
  这个问题有点头疼,总之,尽量提高我们的代码设计和写UT的速度,以便应对各种不合理的需求和项目。
  前面三个问题,或多或少是心态的问题,调整好心态,认可UT的优点,尝试走第一步看看。而第四个问题,如何写?则是笔者这系列文章的核心内容,在我的第一篇《Robolectric3.0(一)》中已经介绍了这个框架的特点,环境搭建,三大组件(Activity、Bordercast、Service)的测试,以及Shadow的使用,这篇文章,主要介绍网络请求和数据库相关的功能如何测试。
  二、日志输出
  Robolectric对日志输出的支持其实非常简单,为什么把它单独列一个条目来讲解?因为往往我们在写UT的过程,其实也是在调试代码,而日志输出对于代码调试起到极大的作用。我们只需要在每个TestCase的setUp()里执行ShadowLog.stream = System.out即可,如:
  @Before
  public void setUp() throws URISyntaxException {
  //输出日志
  ShadowLog.stream = System.out;
  }
  此时,无论是功能代码还是测试代码中的 Log.i()之类的相关日志都将输出在控制面板中,调试起功能来,简直爽得不要不要的。
  三、网络请求篇
  关于网络请求,笔者采用的是retrofit2的2.0.0-beta4版本,api调用有很大的变化,详情请参考官方文档。Robolectic支持发送真实的网络请求,通过对响应结果进行测试,可大大的提升我们与服务端的联调效率。
  以github api为例,网络请求的代码如下:
public interface GithubService {
String BASE_URL = "https://api.github.com/";
@GET("users/{username}/repos")
Call<List<Repository>> publicRepositories(@Path("username") String username);
@GET("users/{username}/following")
Call<List<User>> followingUser(@Path("username") String username);
@GET("users/{username}")
Call<User> user(@Path("username") String username);
class Factory {
public static GithubService create() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
return retrofit.create(GithubService.class);
}
}
}
  1. 测试真实的网络请求
  @Test
  public void publicRepositories() throws IOException {
  Call<List<Repository>> call = githubService.publicRepositories("geniusmart");
  Response<List<Repository>> execute = call.execute();
  List<Repository> list = execute.body();
  //可输出完整的响应结果,帮助我们调试代码
  Log.i(TAG,new Gson().toJson(list));
  assertTrue(list.size()>0);
  assertNotNull(list.get(0).name);
  }
  这类测试的意义在于:
  (1)检验网络接口的稳定性
  (2)检验部分响应结果数据的完整性(如非空验证)
  (3)方便开发阶段的联调(通过UT联调的效率远高于run app后联调)
  2. 模拟网络请求
  对于网络请求的测试,我们需要知道确切的响应结果值,才可进行一系列相关的业务功能的断言(比如请求成功/失败后的异步回调函数里的逻辑),而发送真实的网络请求时,其返回结果往往是不可控的,因此对网络请求和响应结果进行模拟显得特别必要。
  那么如何模拟?其原理很简单,okhttp提供了拦截器 Interceptors ,通过该api,我们可以拦截网络请求,根据请求路径,不进行请求的发送,而直接返回我们自定义好的相应的response json字符串。
  首先,自定义Interceptors的代码如下:
public class MockInterceptor implements Interceptor {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
String responseString = createResponseBody(chain);
Response response = new Response.Builder()
.code(200)
.message(responseString)
.request(chain.request())
.protocol(Protocol.HTTP_1_0)
.body(ResponseBody.create(MediaType.parse("application/json"), responseString.getBytes()))
.addHeader("content-type", "application/json")
.build();
return response;
}
/**
* 读文件获取json字符串,生成ResponseBody
*
* @param chain
* @return
*/
private String createResponseBody(Chain chain) {
String responseString = null;
HttpUrl uri = chain.request().url();
String path = uri.url().getPath();
if (path.matches("^(/users/)+[^/]*+(/repos)$")) {//匹配/users/{username}/repos
responseString = getResponseString("users_repos.json");
} else if (path.matches("^(/users/)+[^/]+(/following)$")) {//匹配/users/{username}/following
responseString = getResponseString("users_following.json");
} else if (path.matches("^(/users/)+[^/]*+$")) {//匹配/users/{username}
responseString = getResponseString("users.json");
}
return responseString;
}
}
  相应的resonse json的文件可以存放在test/resources/json/下,如下图
  
response的json数据文件
  再次,定义Http Client,并添加拦截器:
  //获取测试json文件地址
  jsonFullPath = getClass().getResource(JSON_ROOT_PATH).toURI().getPath();
  //定义Http Client,并添加拦截器
  OkHttpClient okHttpClient = new OkHttpClient.Builder()
  .addInterceptor(new MockInterceptor(jsonFullPath))
  .build();
  //设置Http Client
  Retrofit retrofit = new Retrofit.Builder()
  .baseUrl(GithubService.BASE_URL)
  .addConverterFactory(GsonConverterFactory.create())
  .client(okHttpClient)
  .build();
  mockGithubService = retrofit.create(GithubService.class);
  最后,就可以使用mockGithubService进行随心所欲的断言了:
  @Test
  public void mockPublicRepositories() throws Exception {
  Response<List<Repository>> repositoryResponse = mockGithubService.publicRepositories("geniusmart").execute();
  assertEquals(repositoryResponse.body().get(5).name, "LoveUT");
  }
  这种做法不仅仅可以在写UT的过程中使用,在开发过程中也可以使用,当服务端的接口开发滞后于客户端的进度时,可以先约定好数据格式,客户端采用模拟网络请求的方式进行开发,此时两个端可以做到不互相依赖。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号