前言ide
在上一篇文章中,提到了如何经过 IoC 的设计,以及 Stub Object 的方式,来独立测试目标对象。函数
这一篇文章,则要说明有哪些设计对象的方式,可让测试或需求变动时,更容易转换。单元测试
并说明这些方式有哪些特性,供读者朋友们在设计时,能够选择适合本身情境的方式来使用。学习
需求说明
当调用目标对象的方法时,指望目标对象的内容能够没必要关注相依于哪些实体对象,而只须要依赖于某个接口,经过这样的方式来达到设计的弹性与可独立测试性。测试
那么,有哪一些方式能够达到这样的目的呢?this
构造函数(constructor)
描述:spa
上一篇文章范例所使用的方式,将对象的相依接口,拉到公开的构造函数,供外部对象使用时,可自行组合目标对象的依赖对象实体。.net
public class Validation
{
private IAccountDao _accountDao;
private IHash _hash;
public Validation(IAccountDao dao, IHash hash)
{
this._accountDao = dao;
this._hash = hash;
}
public bool CheckAuthentication(string id, string password)
{
var passwordByDao = this._accountDao.GetPassword(id);
var hashResult = this._hash.GetHashResult(password);
return passwordByDao == hashResult;
}
}
好处:设计
有许多 DI framework 支持 Autowiring。调试
Autowiring is an automatic detection of dependency injection points.
这里的 dependency injection points 在这例子,指的就是构造函数。以 Unity 为例,在 UnityContainer 取得目标对象时,会自动寻找目标对象参数最多的构造函数。并针对每个参数的类型,继续在 UnityContainer 中寻找对应的实体对象,直到目标对象组合完毕,回传一个完整的目标对象。
由构造函数传入依赖接口的实体对象,是一个很通用的方式。所以在结合许多常见的 DI framework,不须要再额外处理。
顾虑点:
当对象愈来愈复杂时,构造函数也会趋于复杂。假若没有 DI framework 的辅助,则使用对象上,面对许多 overload 的构造函数,或是一个构造函数的参数有好几个,会形成使用目标对象上的困难与疑惑。若没有好好进行 refactoring,也可能所以而埋藏许多 bad smell。
另外,假若是许多构造函数,也可能形成要调用 A 方法时,应选用 A 对应的构造函数,但在使用对象上,可能会用错构造函数而不自知,若方法中没有正确的防呆,则可能出现错误。(请搭配单元测试的测试案例来辅助)
最后,与本来直接依赖的程序代码相比较,目标对象的相依对象所以暴露出来,交由外部决定,而丧失了一点封装的意味。而使用端也不必定知道,要取用此对象时,应该要注入哪些相依对象。(请使用 Repository Pattern 或 DI framework 来辅助)
公开属性(public setter property)
描述:
其实公开属性与公开构造函数很是相似,经过 public 的 property(property 类型仍为 interface),让外部在使用目标对象时,可先 setting 目标对象的相依对象,接着才调用其方法。
而公开属性一般只会将 setter 公开给外部设定,getter 则设定为 private。缘由很简单,外部只需设定,而不需取用。就像公开构造函数,在使用对象以前先传入初始化对象必备的信息,但目标对象可能将这些信息,存放在 private 的 filed 或 property 中,而不需再提供给外部使用。
程序代码以下:
public class Validation
{
public bool CheckAuthentication(string id, string password)
{
var accountDao = GetAccountDao();
var passwordByDao = accountDao.GetPassword(id);
var hash = GetHash();
var hashResult = hash.GetHashResult(password);
return passwordByDao == hashResult;
}
private Hash GetHash()
{
var hash = new Hash();
return hash;
}
private AccountDao GetAccountDao()
{
var accountDao = new AccountDao();
return accountDao;
}
}
没什么改变,对吧?
接下来,将两个 new 对象的方法,声明为 protected virtual
,表明子类别能够继承与重写该方法。程序代码以下:
protected virtual Hash GetHash()
{
var hash = new Hash();
return hash;
}
protected virtual AccountDao GetAccountDao()
{
var accountDao = new AccountDao();
return accountDao;
}
另外,将要使用到 Hash 与 AccountDao 的方法,也要声明为 virtual
。程序代码以下:
public class AccountDao
{
public virtual string GetPassword(string id)
{
throw new NotImplementedException();
}
}
public class Hash
{
public virtual string GetHashResult(string password)
{
throw new NotImplementedException();
}
}
到这里,都不影响外部使用目标对象的行为,咱们只是在重构对象的内部方法罢了。事实上,咱们可测试性的动做也准备完毕了。(固然,建议仍是要依赖于接口,实现接口要顾虑的点,比继承类要轻松的多)
接下来把目光切到测试程序,该如何对 CheckAuthentication 方法进行测试。
首先,将上一篇文章的 StubHash 改成继承自 Hash,StubAccountDao 改成继承自 AccountDao,并将本来 public 的方法,加上 override
关键词,重写其父类方法内容。程序代码以下:
public class StubAccountDao : AccountDao
{
public override string GetPassword(string id)
{
return "Hello World";
}
}
public class StubHash : Hash
{
public override string GetHashResult(string password)
{
return "Hello World";
}
}
不难,对吧。接下来,创建一个 MyValidation 的 class,继承自 Validation。并重写 GetAccountDao() 与 GetHash(),使其回传 Stub Object。程序代码以下:
public class MyValidation : Validation
{
protected override AccountDao GetAccountDao()
{
return new StubAccountDao();
}
protected override Hash GetHash()
{
return new StubHash();
}
}
也不难,对吧。接下来,来设计单元测试,程序代码以下:
[TestMethod()]
public void CheckAuthenticationTest()
{
Validation target = new MyValidation();
string id = "id随便";
string password = "密码也随便";
bool expected = true;
bool actual;
actual = target.CheckAuthentication(id, password);
Assert.AreEqual(expected, actual);
}
本来初始化的测试目标为 Validation 对象,如今则为 MyValidation 对象。里面惟一不一样的部分,只有重写的方法内容,其他 MyValidation 就等同于 Validation。(Is-A的关系)调试测试一下,就能够确认,程序代码就跟以前使用 IoC 的方式执行没有太大的差别。
好处:
这个方式最大的好处,是彻底不影响外部使用对象的方式。仅透过 protected 与 virtual 来对继承链开放扩充的功能,而且透过这样的方式,就使得本来直接相依而致使没法测试的问题,得到解套。
顾虑点:
这是为了测试,且面对 legacy code 所使用的方式,而不是良好的面向对象设计的方式。IoC 的用意在于面向借口与扩充点的弹性,因此当可测试以后,假若重构影响范围不大,建议读者朋友仍是要将对象改依赖于接口,经过IoC 的方式来设计对象。
by the way, 一样为了解决直接相依对象,甚至相依于 static 方法、.net framework 自己的对象(如 DateTime.Now)而致使没法测试的问题,还有另一个方式,称为 fake object。这在后面的文章,会再进行较为详尽的介绍。
结论
以上几种用来测试的方式,但愿对各位读者在不一样情境下的设计,能够有所帮助。
而许多延伸的议题,在这系列文章并不会多谈,但在实务应用面上,倒是至关重要的配套措施。例如一再提到的 DI framework, Repository Pattern,以及经过测试程序来讲明对象的使用方式,请读者在现实设计系统时,务必了解这些东西如何让系统设计更加完整。
下一篇文章,将介绍怎么样能够避免每次手工敲打这么啰唆的 stub 对象,怎么针对 static 或 .net framework 自己的对象进行隔离,怎么针对对象与相依接口互动的状况进行测试。
备注:这个系列是我毕业后时隔一年从新开始进入开发行业后对大拿们的博文摘要整理进行学习对自个人各个欠缺的方面进行充电记录博客的过程,非原创,特此感谢91 等前辈