〇、目录
一、前言
二、需求说明
三、准备工作
四、开工
五、总结
六、源码下载
一、前言
上文书(基于VS2012 Fakes框架的TDD实战——接口模拟)把接口模拟的部分演示完了,接口模拟也是Mock框架最基本的功能了吧,比如很易用的Moq框架,就非常容易模拟出接口中定义的操作返回的结果。
Moq也有局限性,比如不能模拟密封类,不能直接模拟静态方法等,而这些需求在微软VS2012带来的Fakes框架中都能得到很好的解决。
二、需求说明
一个项目的开发中,最见怪不怪的就是需求的变更了,比如我们这个用户名重复性检查的功能,它就变了,变化如下:
- 给未激活用户信息添加有效期属性,防止用户名被恶意占用
三、准备工作
修改MemberInactive类如下:
1 public class MemberInactive : Entity 2 { 3 public string UserName { get; set; } 4 5 public string Password { get; set; } 6 7 public string Email { get; set; } 8 9 /// <summary> 10 /// 激活过期时间 11 /// </summary> 12 public DateTime Expiration { get; set; } 13 }
四、开工
- 编写测试用例与实现代码
先编写一个检查未激活用户信息有效性的方法的测试用例,现在是2012年8月26日,所以定MemberInactive的过期时间为2012年8月27日。方便起见,我们先把IsMemberInactiveValid方法的可访问性定为public,用使可以用原来的方式来进行测试
1 [TestMethod] 2 public void IsMemberInactiveValid_有效的_过期时间大于当前时间() 3 { 4 var memberInactive = new MemberInactive { Expiration = new DateTime(2012, 8, 27) }; 5 Assert.IsTrue(_accountService.IsMemberInactiveValid(memberInactive)); 6 }
在类AccountService编写IsMemberInactiveValid方法让测试通过
1 public bool IsMemberInactiveValid(MemberInactive memberInactive) 2 { 3 var dtNow = DateTime.Now; 4 return memberInactive.Expiration.CompareTo(dtNow) >= 0; 5 }
-
静态属性的模拟
测试通过了,但上面的测试用例有个问题,今天能跑通过,后天呢,到了28号,就注定是失败的了,因为实现方法中有一个外部依赖DateTime.Now,自动化测试中,方法体范围内的所有外部依赖都应该被模拟即你要测的仅是这个方法内的代码的正确性,不应该受外界影响。现在我们来模拟DateTime.Now,这是一个静态的公共属性。在mscorlib.dll程序集System命名空间下实现的。所以需要创建System的Fakes程序集。静态成员的模拟将用到Shim类型的模拟类(Fakes框架生成的模拟类有两种,Stub和Shim,具体请参考官方文档)修改上面的测试用例如下:
1 [TestMethod] 2 public void IsMemberInactiveValid_有效的_过期时间大于当前时间() 3 { 4 var memberInactive = new MemberInactive { Expiration = new DateTime(2012, 8, 27) }; 5 //Assert.IsTrue(_accountService.IsMemberInactiveValid(memberInactive)); 6 using (ShimsContext.Create()) 7 { 8 ShimDateTime.NowGet = () => new DateTime(2012, 8, 26); 9 Assert.IsTrue(_accountService.IsMemberInactiveValid(memberInactive)); 10 } 11 }
第8行即模拟了DateTime.Now的返回值,这时,即使你把系统时间修改为28号,这个测试也能通过,因为现在测试的运行已经与系统时间无关了。
- 私有方法的测试
上面的例子为了承接上篇写测试用例的方法把IsMemberInactiveValid方法设成了public,但实际上这个方法应该是私有的,现在把方法的可访问性改为private,原来的测试用例当然是不能通过的,因为这个方法找不到了。把测试用例改为如下:
[TestMethod] public void IsMemberInactiveValid_有效的_过期时间大于当前时间() { var memberInactive = new MemberInactive { Expiration = new DateTime(2012, 8, 27) }; using (ShimsContext.Create()) { ShimDateTime.NowGet = () => new DateTime(2012, 8, 26); var po = new PrivateObject(new AccountService()); var result = po.Invoke("IsMemberInactiveValid", new object[] {memberInactive}); Assert.IsTrue((bool) result); } }
测试通过,私有成员的访问用到了PrivateObject,其实这个类也没什么奇特的地方,只是封装了反射的相关操作,让我们调用更方便些
- 私有方法的模拟
在把调用IsMemberInactiveValid的代码加入UserNameExistsCheck方法之前,千万别忘记了在测试类初始化的代码中把IsMemberInactiveValid模拟出来,否则加入之后原来的测试用例就有可能无法通过了。下面这个测试用例就通不过了
1 [TestMethod] 2 public void UserNameExistsCheck_用户存在_用户在用户数据库中不存在_and_注册需要激活_用户在未激活用户数据库中存在() 3 { 4 var userName = "柳柳英侠"; 5 var configName = "configName"; 6 _member = null; 7 _configInfo.RegisterConfig.NeedActive = true; 8 Assert.IsTrue(_accountService.UserNameExistsCheck(userName, configName)); 9 }
不过这个问题先放下,我们先来看看私有成员应该怎样来模拟,将用到AccountService类的模拟类,因为这个私有方法是这个类的成员,如下的测试用例:
1 [TestMethod] 2 public void UserNameExistsCheck_用户存在_用户在用户数据库中不存在_and_注册需要激活_and_用户在未激活用户数据库中存在_and_未激活用户信息有效() 3 { 4 var userName = "柳柳英侠"; 5 var configName = "configName"; 6 _member = null; 7 _configInfo.RegisterConfig.NeedActive = true; 8 using (ShimsContext.Create()) 9 { 10 ShimAccountService.AllInstances.IsMemberInactiveValidMemberInactive = (@accountService, @memberInactive) => true; 11 Assert.IsTrue(_accountService.UserNameExistsCheck(userName, configName)); 12 } 13 }
根据测试用例修改UserNameExistsCheck方法代码如下(第26行)
1 public bool UserNameExistsCheck(string userName, string configName) 2 { 3 if (string.IsNullOrEmpty(userName)) 4 { 5 throw new ArgumentNullException("userName"); 6 } 7 if (string.IsNullOrEmpty(configName)) 8 { 9 throw new ArgumentNullException("configName"); 10 } 11 var member = MemberDao.GetByName(userName); 12 if (member != null) 13 { 14 return true; 15 } 16 var configInfo = ConfigInfoDao.GetByName(configName); 17 if (configInfo == null) 18 { 19 throw new NullReferenceException("系统配置信息为空。"); 20 } 21 if (!configInfo.RegisterConfig.NeedActive) 22 { 23 return false; 24 } 25 var memberInactive = MemberInactiveDao.GetByName(userName); 26 if (memberInactive != null && IsMemberInactiveValid(memberInactive)) 27 { 28 return true; 29 } 30 return false; 31 }
测试通过。
现在来解决那个未通过的测试用例,按照TDD的原则,我们不能去修改测试用例来使它通过。
不能通过的原因也就是IsMemberInactiveValid的模拟没有在测试类中进行初始化,下面我们来初始化它。由上面的测试用例可以看到,在私有成员的模拟中,测试方法的执行结果必须放在using语句中,而using语句实质也就是自动化了IDisposable接口,所以我们完全可以把它拆开,然后手动调用Dispose即可
在测试类AccountServiceTest添加一个私有字段来存储ShimsContext.Create(),一个私有字段存储 IsMemberInactiveValid的模拟结果:1 private IDisposable _shimsContext = ShimsContext.Create(); 2 private bool _isMemberInactiveValid = true;
在标记[TestInitialize]的MyTestInitialize方法中添加 IsMemberInactiveValid 方法的模拟
1 ShimAccountService.AllInstances.IsMemberInactiveValidMemberInactive = (@accountService, @memberInactive) => _isMemberInactiveValid;
取消标记[TestCleanup()]的MyTestCleanup方法的注释,添加 ShimsContext.Create() 的 Dispose 调用
1 // 在每个测试运行完之后,使用 TestCleanup 来运行代码 2 [TestCleanup()] 3 public void MyTestCleanup() 4 { 5 _shimsContext.Dispose(); 6 }
这样,初始化完毕,再运行全部测试用例,全绿,心情大好└(^o^)┘
五、总结
总结说点什么呢,总结一下TDD吧
开发过程:
- 快速新增一个测试
- 运行所有的测试(有时候只需要运行一个或一部分),发现新增的测试不能通过
- 做一些小小的改动,尽快地让测试程序可运行,为此可以在程序中使用一些不合情理的方法
- 运行所有的测试,并且全部通过
- 重构代码,以消除重复设计,优化设计结构
优点:
- 在开发过程的任意时刻,都可以生成一个可以使用,具有一定功能,Bug较少的测试版本
- 新增的功能不会破坏已有功能
- 测试用例已经包含业务需求和规则,是最符合实际,与时俱进的开发文档
- 规则长期保留并明确
缺点:
- 代码量大大增加(其实只是把需求先体验在代码上,传统的开发方式先需求体验在脑中)
- 凭空编写测试用例(其实并不凭空,只需要把代码运行的环境,涉及的底层模块想清楚,就能立即体现到测试用例上)
其实说白了,TDD与先代码后测试的开发方式的区别,只是前者是把写代码前在脑中的想法体现在测试用例上,一个动手写了,一个在脑中构思而已,就这么简单,只要立即动手把想法以测试用例展现出来,就跨出TDD的第一步了,勇敢的跨出第一步吧