TDD 强迫你 Program to Interface
还是接上次的内容,继续测试Dollar class
先在有个新的需求--在使用Times方法之前,必须要做用户的身份验证,有权限的人才可以用这个方法,反之则不行。(后面称 需求(1))
在做完设计后,我们界定有个class 叫LoginChecker中的方法CheckPass将用来做权限的审查,返回值为bool型,如果有权限返回True, 反之为false。
首先看一下 如果不用TDD 我们脑中第一反应的功能代码实现,应该会是下面的样子--我们去new 了一个LoginChecker的实例,然后调用CheckPass的方法。
//method to be tested
public int Times(int multiplier)
{
var checker = new LoginChecker();
if (checker.CheckPass())
{
return this.amount *= multiplier;
}
return 0;
}
这样我们Dollar class就紧密依赖LoginChecker class了。 如果我来实现 times方法,我可能会有以下两种处理方式:1.实现功能我自己Times的功能,但不做测试(理由是:CheckPass 还没写好,我怎么测试啊,测了也没用,可能CheckPass会抛异常)2. 等CheckPass写完了,我再写Times方法。你是否有嗅出这两种方式写出来的测试都很像集成测试?!TDD是讲究Isolation(独立,隔离)的。这里你要测的就是Times方法,其它所有的dependency(依赖)都应该用Stub(mock,fake找一个你喜欢的词,不过他们是有区别的)来替代。现在的问题是“这个怎么测呢?”一般有这个问题出现时,你的第一反应应该是 “设计是否有问题?为什么要绑定到一个特定的class?”。我们来看看怎么解决,把开始的问题换个方式问“能否不绑定到特定的class?”,可以的 那就是要把功能抽象出来,抽象成Abstract class或者Interface咯。那我们就抽象出一个IChecker的Interface吧
public interface IChecker
{
bool CheckPass();
}
让LoginChecker实现一下,不用写功能哦。
public class LoginChecker : IChecker
{
#region IChecker Members
public bool CheckPass()
{
throw new NotImplementedException();
}
#endregion
}
下面看看如果光实现 Dollar class的话,实现会是什么样的
public class Dollar
{
int amount;
IChecker checker = null;
////method to be tested
public int Times(int multiplier)
{
if (checker.CheckPass())
{
return this.amount *= multiplier;
}
return 0;
}
#endregion
}
从上面代码中,有人肯定要问了,checker这个Instance 怎么来呢?这里可以用到dependency Injection了,一般depency Injection有两种 分别为contructor和setter, 当然factory 也是可以实现的,这里我们就用contructor injection来实现吧。
看看现在的 真正实现代码是什么?其实应该是以下这个样子,因为我们还没有 需求(1)。只是一番思考之后这个需求引导我们 应该Program to Interface然后用Depency Injection,这样才好测。
public class Dollar
{
int amount;
IChecker checker = null;
public Dollar(int amount, IChecker checker)
{
this.amount = amount;
this.checker = checker;
}
////method to be tested
public int Times(int multiplier)
{
return this.amount *= multiplier;
}
#endregion
}
那我们现在开始写需求(1)的测试代码,比方说先测一个简单的:如果Check没过,就总是返回 0
第一步,我们先要写一个stub,让它来替换掉,LoginChecker的CheckPass方法,有了stub你就可以完全控制你的测试了,即使LoginChecker还根本没实现
class LoginCheckerStub : IChecker
{
#region IChecker Members
public bool CheckPass()
{
return false;
}
#endregion
}
真正的test case
///// <summary>
/////A test for times
/////</summary>
[TestMethod()]
public void TesttimesWithChecker()
{
int amount = 5;
IChecker chcker = new LoginCheckerStub(); // new一个stub
Dollar target = new Dollar(amount, chcker); //用stub来控制你的测试,返回你想要的值
int actual = target.Times(10);
Assert.AreEqual(0, actual);
}
真正的实现代码
////method to be tested
public int timesWithChekerFalseReturn0(int multiplier)
{
if (!checker.CheckPass())
{
return 0;
}
}
如果checker过了,那5Times10应该返回50
第一步,我们先要写一个stub,让它来替换掉,LoginChecker的CheckPass方法,有了stub你就可以完全控制你的测试了,即使LoginChecker还根本没实现
class LoginCheckerStub : IChecker
{
#region IChecker Members
public bool CheckPass()
{
return true;
}
#endregion
}
真正的test case
///// <summary>
/////A test for times
/////</summary>
[TestMethod()]
public void TesttimesWithCheckerTrueToCheck()
{
int amount = 5;
IChecker chcker = new LoginCheckerStub(); // new一个stub
Dollar target = new Dollar(amount, chcker); //用stub来控制你的测试,返回你想要的值
int actual = target.Times(10);
Assert.AreEqual(50, actual);
}
真正的实现代码
////method to be tested
public int timesWithCheker(int multiplier)
{
if (!checker.CheckPass())
{
return 0;
}
return this.amount *= multiplier;
}
那可能有些人会说,我就是不想设计什么接口,用什么依赖注入,我还能用TDD 来写我的实现吗?回答是可以的,不过要用到额外的工具,下面用Typemock实现一下,如果实现绑定某一特定的class 测试代码该怎么写。这段测试对应到文章开头的实现。
/// <summary>
/// Test with Typemock
/// </summary>
[TestMethod]
public void TesttimesWithCheckerByTypemock()
{
MockManager.Init();
Mock CheckerMock = MockManager.MockAll(typeof(LoginChecker));
CheckerMock.ExpectAndReturn("checkPass", true);
int amount = 5; // TODO: Initialize to an appropriate value
Dollar target = new Dollar(amount); // TODO: Initialize to an appropriate value
int actual = target.Times(2);
Assert.AreEqual(10, actual);
MockManager.ClearAll();
}
本文介绍了,TDD会引导你面向接口编程,思考你的设计是否合理,是否高耦合。当然也介绍了 代码一定要高耦合情况下,怎么测试(TypeMock),后面将介绍测试中比较头痛的的问题 -- 处理系统时间和外部资源