这是我去年写的一个小框架,专为自动化测试准备数据。以我个人的经验,进行自动化测试尤其是单元测试,除了技能的障碍外,最大的障碍有两点:1)难以解除依赖,因而无法为相关功能编写独立的测试;2)数据准备困难,导致编写测试的成本高。在我的一篇博客《推行TDD的思考》中有相关总结。尤其在企业级软件系统中,面对的领域相对复杂,被测接口常常需要输入复杂的数据,然后再返回复杂的数据。在面向对象开发中,这些数据常常被建模为对象。我们该怎么实例化这些对象?在单元测试中,我们常常会引入Builder模式,通过Fluent Interface的方式建立类似DSL的构建接口,以便于自由、流畅而可任意组合的方式,帮助编写测试的人实例化他想要创建的对象。然而,一旦这个对象内嵌了多层,或具有极多的属性时,创建就变得极为艰难了。
在ThoughtWorks的一些项目中,尝试使用Yaml来准备数据。有一个极好的框架snakeyaml可以很好地支持我们处理yaml文件。正是基于此,启发我开发了这样一个小框架Sisyphus。它可以帮助更方便地以各种文件形式来准备数据,并提供了统一的接口。目前,支持的格式为我们最常使用的yaml与json。
框架的开发并没有什么技术含量,但框架提供的功能却是基于实际项目中面临的困难逐步演化出来的。例如框架提供的模板功能,数据分节功能,在一开始并没有想到。正是因为这两个功能,让我觉得这个框架还有一些用处。之所以将这个框架命名为Sisyphus,缘由在于我将测试数据视为西西弗推动的那一块大石头,无法承受的如命运一般的沉重,却又不得不用力去承受,如此往返以致时时刻刻。
要使用Sisyphus,可以在build.gradle的构建脚本中添加sonatype提供的Repository依赖:
repositories {
maven{
url 'https://oss.sonatype.org/content/groups/public'
}
mavenCentral();
}
dependencies {
test (
'junit:junit:4.11',
'com.github.agiledon:sisyphus:1.0-SNAPSHOT'
)
}
|
针对Yaml文件,我选择了snakeyaml框架。而对于Json数据,我尝试了两种框架Jackson和Gson。我发现这两个框架各有不足之处。倘若使用Jackson,它要求你要反序列化的类型必须定义默认的构造函数,如果没有定义,则必须声明Annotation:@JsonCreate和@JsonProperty。可是,有时候我们要准备的数据对象,或许是自动生成的,并不能修改该定义。何况,为了进行测试而改变产品代码,是邪恶的,不可取。Gson没有这样的约束,但当我尝试将一段字符串解析为byte[]类型时,发现Gson并不支持。为此,Sisyphus为Json数据提供了两种实现,为了区分,若是Gson实现,则要求测试数据文件的扩展名必须为“.gson”。
模板功能使用了ST4的StringTemplate。我没有使用该框架提供的默认变量标识,而是要求将变量用$符号包裹起来。如果不需要模板,则只需提供一个测试数据文件即可;否则还要定义模板文件,它的扩展名为“.template”。例如针对Json格式的数据,倘若定义了这样的User类:
public class User {
public enum Gender { MALE, FEMALE };
public static class Name {
private String _first, _last;
public String getFirst() { return _first; }
public String getLast() { return _last; }
public void setFirst(String s) { _first = s; }
public void setLast(String s) { _last = s; }
}
private Gender _gender;
private Name _name;
private boolean _isVerified;
private byte[] _userImage;
public Name getName() { return _name; }
public boolean isVerified() { return _isVerified; }
public Gender getGender() { return _gender; }
public byte[] getUserImage() { return _userImage; }
public void setName(Name n) { _name = n; }
public void setVerified(boolean b) { _isVerified = b; }
public void setGender(Gender g) { _gender = g; }
public void setUserImage(byte[] b) { _userImage = b; }
}
|
则可以准备模板文件为:
{
"name" : { "first" : $firstName$, "last" : $lastName$ },
"gender" : "MALE",
"verified" : false,
"userImage" : "Rm9vYmFyIQ=="
}
|
而数据文件则可以是:
# This is multi section sample
firstName = "Joe"
lastName = "Sixpack"
///
firstName = "Bruce"
lastName = "Zhang"
///
firstName = "Yi"
lastName = "Zhang"
|
符号///是分节的标识符,而符号#则为注释,读取数据时会忽略该符号后的所有字符。使用Sisyphus框架,就可以很方便地加载数据文件,从而获得三个User实例。如下测试:
@Test
public void should_compose_multi_user_data_by_parsing_template_file() {
List<User> users = Fixture.from("userWithMultiSections.json")
.withTemplate("template/user.template")
.toList(User.class);
assertThat(users, not(nullValue()));
assertThat(users.get(0).getName().getFirst(), is("Joe"));
assertThat(users.get(0).getName().getLast(), is("Sixpack"));
assertThat(users.get(2).getName().getFirst(), is("Yi"));
assertThat(users.get(2).getName().getLast(), is("Zhang"));
}
|
Sisyphus框架还提供了将实例化好的对象输出为对应格式数据文件的功能。这个功能算是框架提供的一个辅助功能,可以避免手动去准备数据文件。例如我们可以先创建一个User实例,将其输出为yaml格式的数据文件,从而将该文件作为测试数据文件:
@Test
public void should_serialize_specific_object_to_string_with_yaml_format() {
User user = createUser();
String result = FixtureAssist.yaml().print(user, "outputUser");
assertThat(result, is("!!com.github.agiledon.sisyphus.domain.json.User\n" +
"gender: MALE\n" +
"name: {first: Yi, last: Zhang}\n" +
"userImage: !!binary |-\n" +
" MDAwMDExMTE=\n" +
"verified: true\n"));
}
|
框架在加载数据文件时,本身提供了缓存功能,如果重复加载同一个文件,则第二次加载时,并不需要真正去读取文件,从而在一定程度上提高了测试的效率。
框架的入口为Fixture类。若要使用Sisyphus准备数据,通常应调用Fixture的静态方法。框架也提供了对JUnit的支持,通过框架自定义的Rule来加载测试数据,使用方式为:
public class DataProviderRuleTest {
@Rule
public DataProviderRule dataProvider = new DataProviderRule();
@Test
@DataResource(resourceName = "user.json", targetClass = User.class)
public void should_compose_User_data_with_json_format() {
User user = dataProvider.provideData();
assertThat(user, not(nullValue()));
assertThat(user.getName().getFirst(), is("Joe"));
}
@Test
@DataResource(resourceName = "userWithTemplate.json",
templateName = "template/user.template",
targetClass = User.class)
public void should_compose_user_data_by_parsing_template_file() {
User user = dataProvider.provideData();
assertThat(user, not(nullValue()));
assertThat(user.getName().getFirst(), is("Joe"));
assertThat(user.getName().getLast(), is("Sixpack"));
}
}
|
但我个人并不推荐这种方式。使用Fixture更直观,甚至更简单。Sisyphus的源代码可以从我的Github上获得,在其Repository主页,有更多实例介绍。你也可以clone代码后,通过测试代码学习框架的使用。clone代码到本地后,将当前目录转到sisyphus,然后运行gradle build,即可对代码进行编译。若需运行测试,可运行gradle test。由于我使用的IDE为IntelliJ Idea,因此,框架的构建脚本中仅支持IDEA。你可以通过运行gradle idea来生成IntelliJ的项目。