用TDD方式实现老赵的SearchCriteriaBinder
看了老赵的 我的TDD实践:可测试性驱动开发(下),我认为这种开发方式并无问题,不过我感到奇怪的是何以老赵会认为用TDD来完成这个工作就会显得尴尬?下面我用TDD的方式来完成同样的工作,读者可以自行比较和老赵的方法有何区别。
首先需要说明两点:
1. 我不熟悉ASP.Net MVC,自己的机器上也没有装此框架,所以代码仅作为示意,并未编译测试,大家能明白意思就好;
2. 我不能说自己的方法一定是正宗的TDD, 仅仅是自己一直在使用、并且自认为在思想上比较符合TDD的方法。
废话就这么多。按照老赵的意思, 我们需要实现这么一个ModelBinder,它的功能是把通过URL输入的查询条件
/keywords-hello%20world--price-100-200--color-black-red转换成一个对应的SearchCriteria。
那么先来写一个测试用例:
public class SearchCriteriaBinderTest
{
[Test]
public void BindModel()
{
var criteria = (SearchCriteria)new SearchCriteriaBinder().BindModel();
Assert.That(criteria.Price.Min == 100);
Assert.That(criteria.Price.Max == 200);
Assert.That(criteria.Keywords.Contains("hello world");
}
}
接下来实现要测试的类:
{
var modelName = bindingContext.ModelName;
var rawValue = bindingContext.ValueProvider[modelName].RawValue;
var text = HttpUtility.UrlDecode(rawValue.ToString());
var tokenGroups = Tokenize(text);
return Build(tokenGroups);
}
public List<string[]> Tokenize(string text)
{
return new List<string>(new string[] {"keyword", "hello world"},
new string["price", ]);
}
public SearchCriteria Build(IEnumerable<string[]> tokens)
{
return new SearchCriteria { Price = new PriceRange(100,200), Keywords = "hello world", };
}
注意在这个时候Tokenize和Build都是假的方法,它们返回的是一个写死的实现,目的仅仅是为了让这个测试用例通过测试。这个假的实现是不是就没有意义呢?不是的,因为此时我们的关注点是BindModel这个方法是否正确实现了。因此,只要这个测试用例通过了,我们就可以确信:只要Tokenize和Build方法返回了正确的值,那么BindModel方法的逻辑就是正确的。至于这两个方法的正确性如何保证,接下来我们会写另外的测试来证明。
对于Tokenize方法的测试用例很好写,但是继续使用上面一个用例的测试数据无法测出该方法是否正确实现了,因此不妨换个数据:
public void Tokenize()
{
var tokens = new new SearchCriteriaBinder().Tokenize("keywords-happy%birthday--price-200-300--color-white");
Assert.That(tokens.Contains("keywords"));
Assert.That(tokens.Contains("price"));
}
这时Tokenize方法的测试就会失败,然后我们再将该方法重构成正规的实现即可。
对Build方法也是同样处理,因为过程是完全一样的,我就偷懒不写了。
我猜想老赵之所以认为TDD“很难设计成一个完整的test case” ,是不是误以为TDD的方法需要对测试的方法把该方法涉及的所有行为细节都测试到?又或者不能接受“开始先写一个假的实现”这种做法?实际上我认为TDD的做法是有好处的,因为TDD让你首先关注BindModel这个方法的实现,这才是我们的核心目标,而Tokenize/Build这两个方法的实现仅仅是为了实现最终目标而不得不完成的附属任务。按照老赵的做法,我们需要先把BindModel实现一半,发现需要Tokenize就转去实现Tokenize,然后再返回来实现BindModel...这样做不是不可行,但是需要程序员需要很强的Context Switch能力,如果缺乏此能力的话,那么我认为还是TDD这种“一次只关注一个方法”的做法更加合适。