以下内容为原创,欢迎转载,转载请注明

来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5795091.html

使用自定义JUnit Rules、annotations和Resources进行单元测试

原文:http://www.thedroidsonroids.com/blog/android/unit-tests-rules-annotations-resources

简介

Unit Test并不只有断言和测试方法组成。它有一些可以用来提高质量和测试代码可读性的技术。在本文中我们将探索:

  • annotations
  • JUnit rules
  • java resources

背景

很多或者大多数Android apps作为一个API Client,因此需要数据格式之间的转换(通常是JSON)和POJO(数据模型类)。我们不需要在自己的代码中实现一个转换引擎而是可以使用如 GSONmoshi 等三方库来完成。

众所周知的库通常都是有很高的单元测试的覆盖率的,所以如下测试它们是没有意义的:

@Test
public void testGson() {
//given
Gson gson = new Gson();
//when
String result = gson.fromJson("\"test\"", String.class);
//then
assertThat(result).isEqualTo("test");
}

Listing 1. 无用的GSON单元测试.

另一方面测试解析(JSON到POJO)和生成(POJO到JSON)逻辑相关的模型类可能是有用的。如下的POJO:

public class Contributor {
public String login;
public boolean siteAdmin;
public long id;
}

Listing 2. 简单POJO.

和相应的JSON:

{
"login": "koral--",
"id": 3340954,
"site_admin": true
}

Listing 3. 简单JSON.

如果属性映射都正确的话,我们希望去测试它。注意属性siteAdmin使用了不同的命名风格 - Java中的驼峰命名和JSON中的蛇底命名。

简单方案

最简单的一种unit test看起来如下:

@Test
public void testParseHardcodedContributors() throws Exception {
//given
String json = "[\n" +
" {\n" +
" \"login\": \"koral--\",\n" +
" \"id\": 3340954,\n" +
" \"site_admin\": true\n" +
" },\n" +
" {\n" +
" \"login\": \"Wavesonics\",\n" +
" \"id\": 406473,\n" +
" \"site_admin\": false\n" +
" }\n" +
"]\n"; GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
Gson gson = gsonBuilder.create(); //when
Contributor[] contributors;
try (Reader reader = new BufferedReader(new StringReader(json))) {
contributors = gson.fromJson(reader, Contributor[].class);
}

Listing 4. 使用硬编码JSON的单元测试.

这种方法有几个弊端。最值得注意的就是比较差的JSON可读性,有大量的转义字符和没有语法高亮。此外有一点模版代码,如果有更多的JSON需要测试的话将会产生重复代码。让我们思考怎样可以用更加简便的方法来编写,提高可读性和消除代码重复率。

改进

首先Gson对象可以在测试方法外部实例化,比如使用一些像 [Dagger] (http://google.github.io/dagger) 的DI(依赖注入)机制或者使用一个简单的常量。DI已经超出了本文的范围所以我们在例子代码中使用后者。在代码提取后看起来如下:

public final class Constants {
public static final Gson GSON; static {
final GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
GSON = gsonBuilder.create();
}
}

Listing 5. 把GSON实例作为一个全局变量.

接着,文本形式的JSON可以被置于一个resource file中。这会给我们带来语法的高亮和缩进(漂亮的打印),默认情况下Android Studio和Intellij IDEA内置了这些功能特性。不需要引号的转义,所以可读性也不再是问题。再者,文件中的行数和列数和GSON中的一致,所以将会更加容易地像这样debug异常:MalformedJsonException: Unterminated array at line 4 column 5 path $[2]。如果JSON被放置在一个单独的文件,行数是会被确切地匹配到的,跟上述硬编码JSON的例子矛盾的地方是,需要通过java源文件中的偏移进行调整。下面是这个示例中被使用的文件:

[
{
"login": "koral--",
"id": 3340954,
"site_admin": true
},
{
"login": "Wavesonics",
"id": 406473,
"site_admin": false
}
]

Listing 6. 包含JSON的Java resource文件.

最后,代码执行转换可以从测试方法中提取,所以广义说它会更容易在不同的测试用例上使用。它可以使用下章将会讨论的Java和JUnit特性来实现。

Goodies

Java resources

Java resources是程序需要的数据文件,它被放置在源代码外面。注意我们讨论的是 Java resources,默认被放置在src/<source set>/resources,并不是 Android App Resources(drawables, layouts等)。这本例子中并没有Android特别的特性。所以一切都是可以像 Robolectric 那样脱离frameworks可单元测试的。

如果listing 6的JSON文件被保存于src/test/resources/pl/droidsonroids/modeltesting/api/contributors.json,它可以通过调用TestClass.getResourceAsStream("contributors.json")来被单元测试代码访问。相关的类需要被放置在对应的package中,在这个例子中是pl.droidsonroids.modeltesting.api。详情见#getResourceAsStream() javadoc

注解Annotation

Annotation是关联到源代码元素的元数据(eg. 方法或者类)。有众所周知的一些如@Override@Deprecated的内置注解。也可以自定义并使用它们把特定的resources绑定到测试方法中。

注解来起来与interface很类似:

Java

@Retention(RUNTIME)
@Target(METHOD)
public @interface JsonFileResource {
String fileName();
Class<?> clazz();
}

Listing 7. 简单注解.

注意interface关键字前面的@符号。我们自定义的注解被2个元注解来注解。我们设置RetentionRUNTIME,因为注解需要在单元测试执行时(运行时)为可读,所以默认的retention(CLASS)的并不满足。我们也需要设置TargetMETHOD因为我们只需要为方法进行注解(绑定特定的resource)。错位的注解会引发编译错误。没有指定一个target,注解会可以被用于任何地方。

JUnit Rules

简单来说,rule是在测试(方法)运行时触发的一个hook。我们将使用rule在测试方法执行之前增加一些额外的行为。即我们将从resources中解析JSON并提供给测试方法内部相应的POJO。我们的目标时像下面这样支持单元测试:

@Rule public JsonParsingRule jsonParsingRule = new JsonParsingRule(Constants.GSON);

@Test
@JsonFileResource(fileName = "contributors.json", clazz = Contributor[].class)
public void testGetContributors() throws Exception {
Contributor[] contributors = jsonParsingRule.getValue();
assertThat(contributors).hasSize(2);
assertThat(contributors[0].login).isEqualTo("koral--");
}

Listing 8. 使用自定义rule的简单测试方法.

如你所见,模版代码与listing 4相比明显地减少。只有必要的部分是类型明确的:

  • GSON实例用来解析JSONs - jsonParsingRule = new JsonParsingRule(Constants.GSON)

  • 被放置JSON字符串的resource - @JsonFileResource(fileName = "contributors.json"

  • POJO类 - , clazz = Contributor[].class

  • POJO实例的接收 - contributors = jsonParsingRule.getValue()

注意对于测试类只需要一个JsonParsingRule实例。对于每个测试方法Rule会被独立计算并且在特定方法中jsonParsingRule.getValue()的结果不会影响到上一次测试。clazz并不是一个错字而是故意的,因为class是Java语言关键字并不能用做一个标识符。还有一个重要的是被@Rule注解的属性必须是public和非static的。

Rule实现

看下rule实现的草案:

public class JsonParsingRule implements TestRule {
private final Gson mGson;
private Object mValue; public JsonParsingRule(Gson gson) {
mGson = gson;
} @SuppressWarnings("unchecked")
public T getValue() {
return (T) mValue;
} @Override
public Statement apply(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
//TODO set mValue according to annotation
base.evaluate();
}
};
}
}

Listing 9. Rule骨架.

我们的rule实现了TestRule,因此可以使用被使用@Rule注解。我们使用了一个范型的getter,所以它的返回值可以被直接分配给特定类型的变量而不需要在测试方法中转型。在apply()方法中我们可以创建一个原始Statement(测试方法)的包装。调用base.evaluate()被放置在最后(在注解处理之后),因此在测试方法执行过程中rule的效果是可见的。

现在更接近地观看statement包装的关键部分(listing 9TODO的实现):

JsonFileResource jsonFileResource = description.getAnnotation(JsonFileResource.class);
if (jsonFileResource != null) {
Class<?> clazz = jsonFileResource.clazz();
String resourceName = jsonFileResource.fileName();
Class<?> testClass = description.getTestClass();
InputStream in = testClass.getResourceAsStream(resourceName); assert in != null : "Failed to load resource: " + resourceName + " from " + testClass;
try (Reader reader = new BufferedReader(new InputStreamReader(in))) {
mValue = mGson.fromJson(reader, clazz);
}
}

Listing 10. Statement的实现.

description参数在这里是必不可少的,它可以让我们访问测试方法包括注解在内的元数据。Rule适用于所有测试方法,包括没有注解的,这种情况下getAnnotation()会返回null,并且我们可以有条件地跳过定制的其余部分。所以测试方法没有@JsonFileResource注解的测试方法(比如,一些不涉及JSON的测试)可以放在使用了JsonParsingRule的测试类中。第8行是下面代码的一个简写等效

if (in != null) {
throw new AssertionError("Failed to load resource: " + resourceName + " from " + testClass);
}

Listing 11. 断言语句判定.

最后我们传入使用被Reader包装的resource到GSON引擎。Try-with-resources语句在这里被使用,所以Reader将会在读取甚至发生异常之后自动关闭。这里需要在finally块中明确类型。

注意try-with-resources从Android API 19(Kitkat)才可用。如果测试代码位于Android gradle module中,并且你的minSdkVersion低于19,那么你可能需要在evaluate()方法上增加@TargetApi(Build.VERSION_CODES.KITKAT)注解来避免lint错误。单元测试会在开发机器(Mac,PC等)上被执行而不是Android设备或者模拟器,所以这里只有compileSdkVersion才是关键。

这样的单元测试(不需要使用Android特定的API)也可以被放在java module中(build.gradleapply plugin: 'java')。理论上这事最好的idea,但是在Android Studio/Intellij IDEA中有一个问题需要预防,那就是从IDE开箱即用地执行单元测试的配置工作。

[Android]使用自定义JUnit Rules、annotations和Resources进行单元测试(翻译)的更多相关文章

  1. Android读取自定义View属性

    Android读取自定义View属性 attrs.xml : <?xml version="1.0" encoding="utf-8"?> < ...

  2. xamarin android checkbox自定义样式

    xamarin android checkbox自定义样式 在drawable文件在新建checkbox_bg.xml文件 <?xml version="1.0" encod ...

  3. [转]Android中自定义checkbox样式

    android中自定义checkbox的图片和大小   其实很简单,分三步: 1.在drawable中创建文件checkbox_selector.xml: <?xml version=" ...

  4. Android RatingBar 自定义样式

    Android RatingBar 自定义样式 1.先定义Style: <style name="RadingStyle" parent="@android:sty ...

  5. Android实现自定义带文字和图片的Button

    Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法. 一.用系统自带的Button实现 最简单的一种办法就 ...

  6. Android Studio 自定义debug签名文件keystore

    Android Studio 自定义debug签名文件keystore

  7. Android之自定义画图文字动画

    结构: BaseView: package com.caiduping.canvas; import android.content.Context; import android.graphics. ...

  8. Android 三档自定义滑动开关,禁止点击功能的实现,用默认的seekbar组件实现

    android三档自定义滑动开关,禁止点击功能的实现,普通开关网上有很多例子,三档滑动开关的则找了整天都没有相关例子,开始用普通开关的源码修改了自己实现了一个类,但效果不如人意,各种边界情况的算法很难 ...

  9. Android 自定义View修炼-【2014年最后的分享啦】Android实现自定义刮刮卡效果View

    一.简介: 今天是2014年最后一天啦,首先在这里,我祝福大家在新的2015年都一个个的新健康,新收入,新顺利,新如意!!! 上一偏,我介绍了用Xfermode实现自定义圆角和椭圆图片view的博文& ...

随机推荐

  1. 一步步开发自己的博客 .NET版(1、基本显示)

    前言 我们每个猿都有一个搭建自己独立博客的梦,我也不例外.以前想 现在想 以后也想.之所以一直迟迟没有着手,是因为难以跨出第一步.每次心里想着,等我以后技术好了再说,然后就没有然后了.以前用过word ...

  2. ENode 2.8 最新架构图简介

    ENode架构图 什么是ENode ENode是一个.NET平台下,纯C#开发的,基于DDD,CQRS,ES,EDA,In-Memory架构风格的,可以帮助开发者开发高并发.高吞吐.可伸缩.可扩展的应 ...

  3. ABP(现代ASP.NET样板开发框架)系列之10、ABP领域层——实体

    点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之10.ABP领域层——实体 ABP是“ASP.NET Boilerplate Project (ASP.NET样板 ...

  4. ABP(现代ASP.NET样板开发框架)系列之17、ABP应用层——参数有效性验证

    点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之17.ABP应用层——参数有效性验证 ABP是“ASP.NET Boilerplate Project (ASP. ...

  5. 基于react实现无限分级菜单

    在开发CMS(内容管理系统)系统时,一般都会用到一个侧边栏或者顶部的二级或者三级菜单,当点击或者鼠标悬浮时,菜单能够随之展开或收起. 本文纯粹为了练习一下react,因此我会在react环境下实现这么 ...

  6. ★Kali信息收集★8.Nmap :端口扫描

    ★Kali信息收集~ 0.Httrack 网站复制机 http://www.cnblogs.com/dunitian/p/5061954.html ★Kali信息收集~ 1.Google Hackin ...

  7. UEditor编辑器使用示例

    1. UEditor下载 UEditor是由百度web前端研发部开发所见即所得富文本web编辑器,具有轻量,可定制,注重用户体验等特点,开源基于MIT协议,允许自由使用和修改代码. 下载链接:http ...

  8. EasyUI中那些不容易被发现的坑——EasyUI重复请求2次的问题

    问题控件:datagrid.combobox.所有能设置url属性的控件 问题版本:1.4.4.1.4.5(之前的版本没测) 问题如图: 重复请求2次,错误代码如图: 错误问题分析:html加载的时候 ...

  9. 软件工程的引入:Scrum开发框架总结

    俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及的知识点如下: 软件工程概念 敏捷开发过程scrum 一.什么是软件工程?请用一句话描述. 软件工程是一门研究性的学科:它用工程化 ...

  10. Hadoop入门学习笔记---part4

    紧接着<Hadoop入门学习笔记---part3>中的继续了解如何用java在程序中操作HDFS. 众所周知,对文件的操作无非是创建,查看,下载,删除.下面我们就开始应用java程序进行操 ...