前言:单元测试的时候经常出现一个对象依耐另一个你无法控制的对象,所以这个时候你必须去替代成一个你自己可以控制的对象来摆脱依耐。
1:为什么要破除依赖1.1:运行速度慢比喻我们要通过用户Id判断用户是否存在,那么我们这个方法就会依赖数据库。这样就成了集成测试,如果大量的测试就会出现速度慢。
1.2:需要配置由于依赖数据库,就会配置和数据库相关的文件。
1.3:一次性测试很多内容,无法定位错误信息比喻这个方法错误可能是由于传入的用户Id为空,也可能传入的用户Id不存在,还可能是数据库连接断开等,这样我们就起不到我们单元测试的目的。
2:存根一个外部依赖项:指的是系统中的一个对象,被测试的代码与这个对象发生交互,但是你不能控制这个对象。比喻前端工程师和后台工程师合作,前端工程师要等待后台返回的数据来处理,那么后台就是他的一个外部依赖项。因为他无法控制后台的代码
定义:一个存根(stub)是对系统中存在的一个依赖项(或协作者)的可控制的替代物(就是你找一个对象来替换你无法控制的对象)。通过使用存根,你在测试代码时无需直接处理这个依赖项。(说白了就是一个你自己定义来对象来取代你无法控制的对象)
3:重构代码设计来提高代码的可测试性3.1:抽取接口使底层可替换 其实底层我们就应该使用接口,这样上层代码依赖的是接口而不是具体的对象,使项目具有更好的扩展性,当然这里做事为了更好的测试。
从底层方法中抽出一个接口
publicinterfaceIUser
{///<summary>///检验用户是否存在///</summary>///<param name="userId">用户名</param>///<returns></returns>boolIsExist(stringuserId);
}
底层访问数据库的类
publicclassUser:IUser
{publicboolIsExist(stringuserId)
{//从数据库查询//如果有返回true}
}
待测试的工作单元
publicboolIsExistUser(stringuserId)
{varuser =newUser();returnuser.IsExist(userId);
}
一个可控制的存根
publicclassFackUser:IUser
{publicboolWillBevalid =false;publicboolIsExist(stringuserId)
{returnWillBevalid;
}
}
下面开始注入存根了。
4:依赖注入(在被测试的单元中注入一个伪实现)4.1:构造参数注入顾名思义就是实例化的时候在构造参数的时候把伪对象注入
此时我们就要修改我们上面的类了如下
被测试类
publicclassUserBll
{PRivatereadonlyIUser _user;publicUserBll(IUser user)
{this._user =user;
}publicboolIsExistUser(stringuserId)
{return_user.IsExist(userId);
}
}
测试代码
[Test]publicvoidIsExistUser_ExistUser_ReturnsTrue()
{varfackUser =newFackUser {WillBevalid =true};varuser =newUserBll(fackUser);//注入伪对象boolresult = user.IsExistUser("1");
Assert.IsTrue(result);
}
关于构造函数注入的总结:使用构造函数注入比较简单直观可读性和理解方面也很不错。但是也有问题就是当你依赖越来越多的时候,加入构造函数的参数越来越多这样就会变得难以维护。
使用场景:比喻api的设计就是某些使用者本身就是带有参数的构造函数那么就可以这么做。
4.2:使用属性(get;set)注入伪对象被测试类
publicclassUserBll
{publicIUser User {get;set; }publicUserBll(IUser user)
{
User=newUser();//默认的情况执行正常对象
}publicboolIsExistUser(stringuserId)
{returnUser.IsExist(userId);
}
}
代码测试
[Test]publicvoidIsGetName_NormalGetName_ReturnsTrue() {varfackUser =newFackUser { WillBevalid =true};varuser =newUserBll { User =fackUser };//属性注入boolresult = user.IsExistUser("1");
Assert.IsTrue(result);
}
关于属性注入总结:和构造函数注入相似不过更易读,更易编写。
什么时候使用属性注入:想表明哪个被测试类的某个依赖项是可选的,或者测试可以放心使用默认创建的这个依赖项,就可以属性注入
4.3:在工厂中伪造一个成员(伪对象)我们先看工厂类
publicclassUserFactory
{privateIUser _user =null;publicIUser Create()
{if(_user !=null)return_user;returnnewUser();
}
[Conditional("DEBUG")]publicvoidSetUser(IUser muser)
{
_user=muser;
}
}
被测试类
publicclassUserBll
{publicboolIsExistUser(stringuserId)
{varuserFactory =newUserFactory();
returnuserFactory.Create().IsExist(userId);
}
测试代码
[Test]publicvoidIsGetName_NormalGetName_ReturnsTrue() {varfackUser =newFackUser { WillBevalid =true};varuserFactory =newUserFactory();
userFactory.SetUser(fackUser);//设置自己要注入的伪对象boolresult =newUserBll().IsExistUser("1");
Assert.IsTrue(result);
}
关于伪造方法的总结: 这种方法很简单,对工厂添加一个你要控制的伪依赖项。对被测试代码没什么改变一切还是原样。
这种方式明显比前两种好。相当于加入了一个工厂的缓冲区。在这里可以做一些逻辑上的处理。
4.4:抽取和重写 使用这种方法的步骤:
在被测试类:
添加一个返回真真实实的虚工厂的方法;在正常的代码中使用工厂方法在测试项目中:
创建一个新类声明这个新类继承被测试类创建一个你要替换的接口类型的公共字段(不需要属性)重写虚方法返回公共字段在测试代码中:
创建一个存根类的实例。此存根实现所要求的接口创建新派生类而非测试类的实例伪造一个工厂方法
publicclassUserBll
{publicboolIsExistUser(stringuserId)
{varuser =UserManager();returnuser.IsExist(userId);}protectedvirtualIUser UserManager()
{returnnewUser();
}
创建新类并集成被测试类
publicclassTestUser : UserBll
{publicTestUser(IUser user) {
_muser=user;
}privatereadonlyIUser _muser;protectedoverrideIUser UserManager() {return_muser;
}
}
测试代码:
[Test]publicvoidIsGetName_NormalGetName_ReturnsTrue() {varfackUser =newFackUser { WillBevalid =true};//存根实例vartestUser =newTestUser(fackUser);//注入伪对象(新派生的类)boolresult = testUser.IsExistUser("1");
Assert.IsTrue(result);
}
关于抽取和重写注入的总结:写更少的接口,代码更容易替换。我觉得这种方法最好,就是留了一条路,不光对于测试,如果哪天发现这代码不好了,直接可以在底层新添加一个替换即可,不会影响原来的代码
什么时候使用:当你调用外部依赖项时候想模拟自己想要的值的时候就特别受用。
4.5:重构技术变种先看被测试类
publicclassUserBll
{publicboolIsExistUser(stringuserId)
{returnUserManager(userId);}protectedvirtualboolUserManager(stringuserId)
{
IUser user=newUser();returnuser.IsExist(userId);
}
}
创建新类并集成被测试类
publicclassTestUser : UserBll
{publicboolIsSupported;protectedboolIsGetUserName(stringuserId) {returnIsSupported;
}
}
测试类
publicvoidIsGetName_NormalGetName_ReturnsTrue() {vartestUser =newTestUser { IsSupported =true};boolresult = testUser.IsExistUser("1");
Assert.IsTrue(result);
}
总结:这和上一种方式其实是很像的,只不过这种更彻底。这种方式更加简单。不在添加很多的构造函数,设置方法或者工厂类。不过确实不符合面向对象中的封装原则。暴露了用户不改看到的东西。
各种依赖注入灵活使用。个人觉得后三种都不错。