简单的程序代码如下(AccountDao与Hash的内容不是重点,为节省篇幅就先省略):
1 using System; 2 3 public class Validation 4 { 5 public bool CheckAuthentication(string id, string password) 6 { 7 // 取得数据库中,id对应的密码 8 AccountDao dao = new AccountDao(); 9 var passwordByDao = dao.GetPassword(id); 11 // 针对传入的password,进行hash运算 12 Hash hash = new Hash(); 13 var hashResult = hash.GetHashResult(password); 15 // 对比hash后的密码,与数据库中的密码是否吻合 16 return passwordByDao == hashResult; 17 } 18 } 19 20 public class AccountDao 21 { 22 internal string GetPassword(string id) 23 { 24 //连接DB 25 throw new NotImplementedException(); 26 } 27 } 28 29 public class Hash 30 { 31 internal string GetHashResult(string passwordByDao) 32 { 33 //使用SHA512 34 throw new NotImplementedException(); 35 } 36 }先将职责分离,所以取得数据是通过AccountDao对象,Hash运算则通过Hash对象。 一切都很合理吧。那么,这样会有什么问题?
[TestMethod()] public void CheckAuthenticationTest() { Validation target = new Validation(); // TODO: 初始化为适当值 string id = string.Empty; // TODO: 初始化为适当值 string password = string.Empty; // TODO:初始化为适当值 bool expected = false; // TODO: 初始化为适当值 bool actual; actual = target.CheckAuthentication(id, password); Assert.AreEqual(expected, actual); Assert.Inconclusive("验证这个测试方法的正确性。"); }不论怎么arrange,当呼叫Validation对象的CheckAuthentication方法时,就肯定会使用AccountDao的GetPassword方法,进而联机至DB,取得对应的密码数据。 还记得我们对单元测试的定义与原则吗?单元测试必须与外部环境、类别、资源、服务独立,而不能直接相依。这样才是单纯的测试目标对象本身的逻辑是否符合预期。 而且单元测试需要运行相当快速,倘若单元测试还需要数据库的资源,那么代表执行单元测试,还需要设定好数据库联机或外部服务设定,并且执行肯定要花些时间。这,其实就是属于整合测试,而非单元测试。
1 public interface IAccountDao 2 { 3 string GetPassword(string id); 4 } 5 6 public interface IHash 7 { 8 string GetHashResult(string password); 9 } 10 11 public class AccountDao : IAccountDao 12 { 13 public string GetPassword(string id) 14 { 15 throw new NotImplementedException(); 16 } 17 } 18 19 public class Hash : IHash 20 { 21 public string GetHashResult(string password) 22 { 23 throw new NotImplementedException(); 24 } 25 } 26 27 public class Validation 28 { 29 private IAccountDao _accountDao; 30 private IHash _hash; 31 32 public Validation(IAccountDao dao, IHash hash) 33 { 34 this._accountDao = dao; 35 this._hash = hash; 36 } 37 38 public bool CheckAuthentication(string id, string password) 39 { 40 // 取得数据库中,id对应的密码 41 var passwordByDao = this._accountDao.GetPassword(id); 42 // 针对传入的password,进行hash运算 43 var hashResult = this._hash.GetHashResult(password); 44 // 对比hash后的密码,与数据库中的密码是否吻合 45 return passwordByDao == hashResult; 46 } 47 }上面可以看到,原本直接相依的对象,现在都通过相依于接口。而 CheckAuthentication 逻辑更加清楚了,如同批注所述: 取得数据中 id 对应的密码 (数据怎么来的,不必关注) 针对 password 进行 hash (怎么 hash 的,不必关注) 针对 hash 结果与数据中存放的密码比对,回传比对结果 类别相依关系如下所示: 这就是面向接口的设计。而原本初始化相依对象的动作,通过目标对象的公开构造函数,可由外部传入接口所属的实例,也就是在目标对象外初始化完成后传入。
控制反转(IoC),它为相互依赖的组件提供抽象,将依赖(低层模块)对象的获得交给第三方(系统)来控制,即依赖对象不在被依赖模块的类中直接通过new来获取 依赖注入(DI),它提供一种机制,将需要依赖(低层模块)对象的引用传递给被依赖(高层模块)对象。把初始化动作,由原本目标对象内,转移到目标对象之外,称作「控制反转」,也就是 IoC。 把依赖的对象,通过目标对象公开构造函数,交给外部来决定,称作「依赖注入」,也就是 DI。 而 IoC 跟 DI,其实就是同一件事:让外部决定目标对象的相依对象。
原文可參考 Martin Fowler 的文章:Inversion of Control Containers and the Dependency Injection pattern
如此一来,目标对象就可以专注于自身的商业逻辑,而不直接相依于任何实体对象,仅相依于接口。而这也是目标对象的扩充点,或是接缝,提供了未来实作新的对象,来进行扩充或转换相依对象模块,而不必修改到目标对象的 context 内容。 通过 IoC 的方式,来隔绝对象之间的相依性,也带来了上述提到的扩充点,这其实就是最基本的可测试性。下一段我们将来介绍,为什么这样的设计,可以提供可测试性。As a result I think we need a more specific name for this pattern. Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection.
[TestMethod()] public void CheckAuthenticationTest() { IAccountDao accountDao = null;// TODO: 初始化为合适的值 Hash hash = null;// TODO: 初始化为合适的值 Validation target = new Validation(accountDao, hash); string id = string.Empty; // TODO: 初始化为合适的值 string password = string.Empty;//TODO: 初始化为合适的值 bool expected = false;// TODO: 初始化为合适的值 bool actual; actual = target.CheckAuthentication(id, password); Assert.AreEqual(expected, actual); Assert.Inconclusive("验证这个测试的正确性。"); }看到了吗?Visual Studio会自动帮我们把构造函数需要的参数也都列出来。 为什么这样的设计方式,就可以帮助我们只独立的测试Validation的CheckAuthentication方法呢? 接下来要用到「手动设计」的stub。 大家回过头看一下,CheckAuthentication方法中,使用到了IAccountDao的GetPassword方法,取得id对应密码。也使用到了IHash的GetHashResult方法,取得hash运算结果。接着才是比对两者是否相同。 通过接口可进行扩充,多态和重载(如果是继承父类或抽象类,而非实作接口时)的特性,我们这边举IAccountDao为例,建立一个StubAccountDao的类型,来实现IAccountDao。并且,在GetPassword方法中,不管传入参数为何,都固定回传"Hello World",代表Dao回来的密码。程序代码如下所示:
public class StubAccountDao : IAccountDao { public string GetPassword(string id) { return "Hello World"; } }
接着用同样的方式,让 StubHash 的 GetHashResult,也回传 "Hello World",代表 hash 后的结果。程序代码如下:
public class StubHash : IHash { public string GetHashResult(string password) { return "Hello World"; } }
聪明的读者朋友们,应该知道接下来就是来写单元测试的 3A pattern,单元测试程序代码如下:
[TestMethod()] public void CheckAuthenticationTest() { //arrange // 初始化StubAccountDao,来当作IAccountDao的执行对象 IAccountDao dao = new StubAccountDao(); // 初始化StubHash,来当作IStubHash的执行对象 IHash hash = new StubHash(); Validation target = new Validation(dao, hash); string id = "随便写"; string password = "随便写"; bool expected = true; bool actual; //act actual = target.CheckAuthentication(id, password); //assert Assert.AreEqual(expected, actual); }如此一来,就可以让我们的测试目标对象:Validation,不直接相依于 AccountDao 与 Hash 对象,通过 stub 对象来模拟,以验证 Validation 对象本身的 CheckAuthentication 方法逻辑,是否符合预期。 测试程序使用 Stub 对象,其类别图如下所示:
备注:这个系列是我毕业后时隔一年重新开始进入开发行业后对大拿们的博文摘要整理进行学习对自我的各个欠缺的方面进行充电记录博客的过程,非原创,特此感谢91 等前辈