由单元测试想到的应用TDD产生的优势——对象之间的低耦合设计

来源:互联网 发布:spss软件安装 编辑:程序博客网 时间:2024/06/07 17:31

最近在看单元测试的文章,看到一个比较好的例子,即由单元测试的可测试性来判断对象之间的耦合程度,顺便印证了一把TDD(Test-Driven Development)测试驱动开发理念的优势——大大降低了对象之间的耦合度。下面请看具体例子吧。


单元测试中有一个比较重要的概念即可测试性,指的是是否可以对目标对象进行独立的测试,所谓独立测试即要隔绝内外部的相关联系,如链接数据库、读取文本、与另一个对象通信等。先举一个紧耦合的例子说明可测试性的失败,由于对象之间的紧耦合关系:


假设现在有一个用户名和密码验证服务,首先它需要从某个数据库或文件读取用户名和密码,并与外部输入的用户名和密码进行hash比对,上代码:

UserValidation类:

public class UserValidation    {        public bool CheckAuth(string userId,string userPwd)        {            //get user password by id from database            AccountDao accountDao = new AccountDao();            string passwordByaccountDao = accountDao.GetPassword(userId);            //hash userPwd from external            HashUtil hashUtil = new HashUtil();            string hashRstByuserPwd = hashUtil.GetHashResult(userPwd);            //Check two password are equal or not equal            return passwordByaccountDao == hashRstByuserPwd;        }    }

AccountDao类:(链接数据库获取密码)
public class AccountDao    {        public string GetPassword(string id)        {            throw new NotImplementedException();        }    }

HashUtil类:(计算外部传入的密码哈希值)

public class HashUtil    {        public string GetHashResult(string pwd)        {            throw new NotImplementedException();        }    }

以上类之间的关系图如下:


CheckAuth方法的单元测试如下:

[TestMethod()]        public void CheckAuthTest()        {            //arrange            UserValidation target = new UserValidation();            string userId = string.Empty;            string userPwd = string.Empty;            bool expected = false;            bool actual;            //act            actual = target.CheckAuth(userId, userPwd);            //assert            Assert.AreEqual(expected, actual);        }


UserValidation类的CheckAuth方法直接依赖于AccountDao类和HashUtil类,导致编写针对CheckAuth方法时必须链接数据库和与HashUtil的通信,这样就违背了单元测试的基本规则——与外部对象隔离进行测试,这样的测试实际上已经是集成测试了,即访问了数据库,又计算了密码的hash值,而CheckAuth真正的核心商业逻辑应该是这样的:

1、获取根据userId对应的用户密码值,数据如何获取,它不必关心;

2、针对外部传入的userPwd进行hash运算,获取密码的hash值,如何进行hash运算,它也不必关心;

3、对1、2步骤的获取的值进行对比,返回对比的结果。

根据以上新的商业逻辑,我们可以修改上述代码如下:

第一步:提取接口IAccountDao

interface IAccountDao    {        string GetPassword(string id);    }

第二步:提取接口IHashUtil

interface IHashUtil    {        string GetHashResult(string pwd);    }

第三步:修改UserValidation类如下:

public class UserValidation    {        private IAccountDao accountDao;        private IHashUtil hashUtil;        public UserValidation(IAccountDao accountDao,IHashUtil hashUtil)        {            this.accountDao = accountDao;            this.hashUtil = hashUtil;        }        public bool CheckAuth(string userId,string userPwd)        {            //get user id and password from database            string passwordByaccountDao = this.accountDao.GetPassword(userId);            //hash userPwd from external            string hashRstByuserPwd = this.hashUtil.GetHashResult(userPwd);            //Check two password are equal or not equal            return passwordByaccountDao == hashRstByuserPwd;        }    }

以上类的关系如下所示:


通过这样的重构,单元测试代码可按照隔离原则编写如下:

[TestMethod()]        public void CheckAuthTest()        {            //arrange            IAccountDao accountDao = null;            IHashUtil hashUtil = null;            UserValidation target = new UserValidation(accountDao,hashUtil);            string userId = string.Empty;            string userPwd = string.Empty;            bool expected = false;            bool actual;            //act            actual = target.CheckAuth(userId, userPwd);            //assert            Assert.AreEqual(expected, actual);        }

当然经过以上的代码编写,单元测试还是无法通过的,因为输入没有,下面引入stub桩对象实现输入模拟:

桩对象StubAccountDao只要实现IAccountDao接口即可满足要求,代码如下:

public class StubAccountDao:IAccountDao    {        public string GetPassword(string id)        {            return "66";        }    }

桩对象StubHashUtil实现IHashUtil接口代码如下:

public class StubHashUtil:IHashUtil    {        public string GetHashResult(string pwd)        {            return "66";        }    }

单元测试代码修改如下:

[TestMethod()]        public void CheckAuthTest()        {            //arrange            IAccountDao accountDao =new StubAccountDao();            IHashUtil hashUtil = new StubHashUtil();            UserValidation target = new UserValidation(accountDao,hashUtil);            string userId = string.Empty;            string userPwd = string.Empty;            bool expected = true;            bool actual;            //act            actual = target.CheckAuth(userId, userPwd);            //assert            Assert.AreEqual(expected, actual);        }

至此,本次单元测试编写完成,从重构失败的产品代码开始,到引入桩对象完成程序输入模拟,最终通过单元测试。

最后,为什么说对TDD是一种印证呢?如果大家反过来先写单元测试代码,会不会直接就定义接口,使用松耦合的设计来编写产品代码呢?

原创粉丝点击