a 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个元注解来注解。我们设置Retention为RUNTIME,因为注解需要在单元测试执行时(运行时)为可读,所以默认的retention(CLASS)的并不满足。我们也需要设置Target为METHOD因为我们只需要为方法进行注解(绑定特定的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 eva luate() throws Throwable {
//TODO set mValue according to annotation
base.eva luate();
}
};
}
}
Listing 9. Rule骨架.
我们的rule实现了TestRule,因此可以使用被使用@Rule
注解。我们使用了一个范型的getter,所以它的返回值可以被直接分配给特定类型的变量而不需要在测试方法中转型。在apply()
方法中我们可以创建一个原始Statement(测试方法)的包装。调用base.eva luate()
被放置在最后(在注解处理之后),因此在测试方法执行过程中rule的效果是可见的。
现在更接近地观看statement包装的关键部分(listing 9中TODO
的实现):
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: &qu