逸言

测试数据准备框架

| Comments

这是我去年写的一个小框架,专为自动化测试准备数据。以我个人的经验,进行自动化测试尤其是单元测试,除了技能的障碍外,最大的障碍有两点: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的项目。

Comments