开发代码时经常需要测试数据的准备工作,有时候为了测试复杂的情况,需要准备的测试数据往往会有一些细微的不同,为了提高测试和测试代码的可维护性,如何让测试数据的准备变得可读、可理解十分重要。前段时间参与了一个项目,随着项目的进展,需要准备的测试用例变化越来越多,因此我们也必须演化测试数据的准备方式。
该项目要实现一个类Blog的功能,而该测试数据准备针对的是Blog列表功能的单元测试和集成测试。我们要测试的逻辑包括分页、排序、查询等,通常要准备几十条(x页)的blog数据。
我们经常可以在测试函数或者Setup函数中看到如下的代码:
[TestFixture]Public void BlogListTest() { protected readonly IList<Blog]]> _blogs; protectedIList<Blog]]>_actualBlogs; [Setup] Public void InitializeResources() { // create three pages of fake blog posts (41 blog posts) for (int i = 0; i < 41; i++) { _blogs.Add(new Blog { Author = "author", Content = "blog number" + i, Title = "test title", Id = "ID00000000" + (i + 1), SubmitUser = "user", Tags = tagList, ... }); } _actualBlogs=_blogs .Where(r.SubmitUser == 'user'") .OrderByDescending(r.CreateTime) .Take(20).ToList(); } [Test] public void OrderBy_CreateTime_Desc() { AssertBlogs_OrderByCreateTime_Descending(_actualBlogs); } } |
这段代码中,让人看得不爽的是测试函数中的初始话数据部分,里面花了很大功夫去创建一个三页的Blog列表,整个函数头重脚轻,这一堆代码对于理解我的测试毫无意义:
第一步,首先要把这种测试数据的创建代码挪到同一个地方,所有的测试用例引用同一个地方去创建测试数据。一个理想的位置当然是Blog类的旁边。
internal static class BlogsGenerator { internal static IList<blog> CountOfBlogs(this int totalCount, string tags) { var tagList = tags.ToTagList(); var blogs = new List<blog>(); 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<Blog> Build() { var countOfBlog = _countOfPage*PageSize - (PageSize - 1); var blogs = new List<Blog>(); 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; } } |