开发代码时经常需要测试数据的准备工作,有时候为了测试复杂的情况,需要准备的测试数据往往会有一些细微的不同,为了提高测试和测试代码的可维护性,如何让测试数据的准备变得可读、可理解十分重要。前段时间参与了一个项目,随着项目的进展,需要准备的测试用例变化越来越多,因此我们也必须演化测试数据的准备方式。
该项目要实现一个类Blog的功能,而该测试数据准备针对的是Blog列表功能的单元测试和集成测试。我们要测试的逻辑包括分页、排序、查询等,通常要准备几十条(x页)的blog数据。
我们经常可以在测试函数或者Setup函数中看到如下的代码:
[TestFixture]Public void BlogListTest()
{
protected readonly IList
这段代码中,让人看得不爽的是测试函数中的初始话数据部分,里面花了很大功夫去创建一个三页的Blog列表,整个函数头重脚轻,这一堆代码对于理解我的测试毫无意义:
它是一种噪音(Noise),为了理解整个测试,必须阅读很多不相关的信息,增加了很多没有必要的阅读量,也就是维护成本。
经常见到这样的代码遍布很多的测试用例,假设需要Blog的字段发生变动,必须要更新所有的测试用例,这也是维护成本
对于Setup中列出的很多字段,很难弄明白哪几个字段对我们的测试是真正有意义的,只能猜,这也是维护成本。
第一步,首先要把这种测试数据的创建代码挪到同一个地方,所有的测试用例引用同一个地方去创建测试数据。一个理想的位置当然是Blog类的旁边。
internal static class BlogsGenerator
{
internal static IList CountOfBlogs(this int totalCount, string tags)
{
var tagList = tags.ToTagList();
var blogs = new List();
for (int i = 0; i < totalCount; i++)
{
blogs.Add(new blog
{
Author = "author",
Content = "blog number" + i,
Title = "test title" + i,
Id = "ID00000000" + (i + 1),
SubmitUser = "user",
Tags = tagList,
。。。
});
}
return blogs;
}
}
}
接下来在所有的单元测试和集成测试中需要Blog列表的测试用例都可以使用这样的代码
[Setup]
Public void InitializeResources()
{
_blog = 41.CountOfBlogs("agile");
_actualBlogs=_blogs
.Where(r.SubmitUser == 'user'")
.OrderByDescending(r.CreateTime)
.Take(20).ToList();
}
与第一种方案相比,Setup函数清晰了很多。但是,还是有一个Magic Number => 41。不理解的人往往需要猜测这个41代表了什么。(其实原意是为了每页有20条,然后有3页,第3页只有1条)。那就不如用代码把背后隐含的意义写清楚
_blogs = BlogListBuilder
.AnBlogList()
.OfPage(3)
.WithDifferentCreateUpdateTime()
.WithTag("agile")
.Build();
实现方式参照[Freeman, Pryce, 2009]的Test Data Builder模式。测试数据创建开起来十分清晰,基本上消除了噪音,而且十分明确的指出了测试数据某些属性的不同之处。具体实现方法其实也很简单,首先在属性赋值函数中设定一些一些变量,然后在Build方法中根据这些变量生成相应的数据。具体如下
public class BlogListBuilder
{
private int _countOfPage = 1;
private const int PageSize = 20;
private bool _differentCreateTime;
private bool _persistable;
private string _tagName = "tag";
public static BlogListBuilder AnBlogList()
{
return new BlogListBuilder();
}
public IList Build()
{
var countOfBlog = _countOfPage*PageSize - (PageSize - 1);
var blogs = new List();
for (int i = 0; i < countOfBlog; i++)
{
var blogBuilder = BlogBuilder.AnBlog().WithTag(_tagName);
if (_differentCreateTime )
{
blogBuilder = blogBuilder.WithCreateUpdateTime(i);
}
blogs.Add(blogBuilder.Build(i));
}
if (_persistable)
{
// save to DB
}
return blog;
}
public BlogListBuilder OfPage(int countOfPage)
{
_countOfPage = countOfPage;
return this;
}
public BlogListBuilder WithDifferentCreateUpdateTime()
{
_differentCreateTime = true;
return this;
}
public BlogListBuilder PersistIntoRepository()
{
_persistable = true;
return this;
}
public BlogListBuilder WithTag(string tagName)
{
_tagName = tagName;
return this;
}
}
