·您现在的位置: 云翼网络 >> 文章中心 >> 网站建设 >> 网站建设开发 >> ASP.NET网站开发 >> 破除依赖

破除依赖

作者:佚名      ASP.NET网站开发编辑:admin      更新时间:2022-07-23

  前言:单元测试的时候经常出现一个对象依耐另一个你无法控制的对象,所以这个时候你必须去替代成一个你自己可以控制的对象来摆脱依耐。

1:为什么要破除依赖

1.1:运行速度慢

比喻我们要通过用户Id判断用户是否存在,那么我们这个方法就会依赖数据库。这样就成了集成测试,如果大量的测试就会出现速度慢。

1.2:需要配置

由于依赖数据库,就会配置和数据库相关的文件。

1.3:一次性测试很多内容,无法定位错误信息

比喻这个方法错误可能是由于传入的用户Id为空,也可能传入的用户Id不存在,还可能是数据库连接断开等,这样我们就起不到我们单元测试的目的。

2:存根

一个外部依赖项:指的是系统中的一个对象,被测试的代码与这个对象发生交互,但是你不能控制这个对象。比喻前端工程师和后台工程师合作,前端工程师要等待后台返回的数据来处理,那么后台就是他的一个外部依赖项。因为他无法控制后台的代码

定义:一个存根(stub)是对系统中存在的一个依赖项(或协作者)的可控制的替代物(就是你找一个对象来替换你无法控制的对象)。通过使用存根,你在测试代码时无需直接处理这个依赖项。(说白了就是一个你自己定义来对象来取代你无法控制的对象)

3:重构代码设计来提高代码的可测试性

3.1:抽取接口使底层可替换

 其实底层我们就应该使用接口,这样上层代码依赖的是接口而不是具体的对象,使项目具有更好的扩展性,当然这里做事为了更好的测试。

从底层方法中抽出一个接口

public interface IUser
    {
        /// <summary>
        /// 检验用户是否存在
        /// </summary>
        /// <param name="userId">用户名</param>
        /// <returns></returns>
        bool IsExist(string userId);
    }

底层访问数据库的类

public class User:IUser
    {
        public bool IsExist(string userId)
        {
            //从数据库查询
            //如果有返回true
        }
    }

待测试的工作单元

public bool IsExistUser(string userId)
        {            
             var user = new User();
             return user.IsExist(userId);
        }                
        

一个可控制的存根

public class FackUser:IUser
    {
        public bool WillBevalid = false;        
        public bool IsExist(string userId)
        {
            return WillBevalid;    
        }
    }

下面开始注入存根了。

4:依赖注入(在被测试的单元中注入一个伪实现)

4.1:构造参数注入

顾名思义就是实例化的时候在构造参数的时候把伪对象注入

此时我们就要修改我们上面的类了如下

被测试类

public class UserBll
    {
        PRivate readonly IUser _user;
        public UserBll(IUser user)
        {
            this._user = user;
        }
        public bool IsExistUser(string userId)
        {            
            return _user.IsExist(userId);                        
        }       
}

测试代码

         [Test]
        public void IsExistUser_ExistUser_ReturnsTrue()
        {
            var fackUser = new FackUser {WillBevalid = true};
            var user = new UserBll(fackUser);//注入伪对象
            bool result = user.IsExistUser("1");
            Assert.IsTrue(result);
        }

 关于构造函数注入的总结:使用构造函数注入比较简单直观可读性和理解方面也很不错。但是也有问题就是当你依赖越来越多的时候,加入构造函数的参数越来越多这样就会变得难以维护。

使用场景:比喻api的设计就是某些使用者本身就是带有参数的构造函数那么就可以这么做。

4.2:使用属性(get;set)注入伪对象

被测试类

public class UserBll
    {
        public IUser User { get; set; }

        public UserBll(IUser user)
        {
            User = new User();//默认的情况执行正常对象
        }

        public bool IsExistUser(string userId)
        {
            return User.IsExist(userId);    
        }       
}

代码测试

       [Test]
        public void IsGetName_NormalGetName_ReturnsTrue() {
            var fackUser = new FackUser { WillBevalid = true };
            var user = new UserBll { User = fackUser };//属性注入
            bool result = user.IsExistUser("1");
            Assert.IsTrue(result);
        }

关于属性注入总结:和构造函数注入相似不过更易读,更易编写。

什么时候使用属性注入:想表明哪个被测试类的某个依赖项是可选的,或者测试可以放心使用默认创建的这个依赖项,就可以属性注入

4.3:在工厂中伪造一个成员(伪对象)

我们先看工厂类

public class UserFactory
    {
        private  IUser _user = null;

        public  IUser Create()
        {
            if (_user != null)
                return _user;
            return new User();
        }

        [Conditional("DEBUG")]
        public  void SetUser(IUser muser)
        {
            _user = muser;
        }
    }

被测试类

public class UserBll
    {        
        public bool IsExistUser(string userId)
        {                            
                var userFactory = new UserFactory();                                          
return userFactory.Create().IsExist(userId); }

测试代码

        [Test]
        public void IsGetName_NormalGetName_ReturnsTrue() {
            var fackUser = new FackUser { WillBevalid = true };
            var userFactory = new UserFactory();
            userFactory.SetUser(fackUser);//设置自己要注入的伪对象
            bool result = new UserBll().IsExistUser("1");
            Assert.IsTrue(result);
        }

关于伪造方法的总结: 这种方法很简单,对工厂添加一个你要控制的伪依赖项。对被测试代码没什么改变一切还是原样。

这种方式明显比前两种好。相当于加入了一个工厂的缓冲区。在这里可以做一些逻辑上的处理。

4.4:抽取和重写

 使用这种方法的步骤:

在被测试类:

  • 添加一个返回真真实实的虚工厂的方法;
  • 在正常的代码中使用工厂方法

在测试项目中:

  • 创建一个新类
  • 声明这个新类继承被测试类
  • 创建一个你要替换的接口类型的公共字段(不需要属性)
  • 重写虚方法
  • 返回公共字段

在测试代码中:

  • 创建一个存根类的实例。此存根实现所要求的接口
  • 创建新派生类而非测试类的实例

伪造一个工厂方法

public class UserBll
    {        
        public bool IsExistUser(string userId)
        {            var user = UserManager();
                return user.IsExist(userId);                     
        }

        protected virtual IUser UserManager()
        {        
            return new User();
        }

创建新类并集成被测试类

public class TestUser : UserBll
    {
        public TestUser(IUser user) {
            _muser = user;
        }

        private readonly IUser _muser;

        protected override IUser UserManager() {
            return _muser;
        }        
    }

测试代码:

        [Test]
        public void IsGetName_NormalGetName_ReturnsTrue() {
            var fackUser = new FackUser { WillBevalid = true };//存根实例
            var testUser = new TestUser(fackUser);//注入伪对象(新派生的类)
            bool result = testUser.IsExistUser("1");
            Assert.IsTrue(result);
        }

关于抽取和重写注入的总结:写更少的接口,代码更容易替换。我觉得这种方法最好,就是留了一条路,不光对于测试,如果哪天发现这代码不好了,直接可以在底层新添加一个替换即可,不会影响原来的代码

什么时候使用:当你调用外部依赖项时候想模拟自己想要的值的时候就特别受用。

4.5:重构技术变种

先看被测试类

public class UserBll
    {        
        public bool IsExistUser(string userId)
        {
return UserManager(userId);         
        }

        protected virtual bool UserManager(string userId)
        {
            IUser user = new User();
            return user.IsExist(userId);
        }        
    }

创建新类并集成被测试类

public class TestUser : UserBll
    {    
        public bool IsSupported;
        protected bool IsGetUserName(string userId) {
            return IsSupported;
        }
    }

测试类

public void IsGetName_NormalGetName_ReturnsTrue() {
            var testUser = new TestUser { IsSupported = true };
            bool result = testUser.IsExistUser("1");
            Assert.IsTrue(result);
        }

总结:这和上一种方式其实是很像的,只不过这种更彻底。这种方式更加简单。不在添加很多的构造函数,设置方法或者工厂类。不过确实不符合面向对象中的封装原则。暴露了用户不改看到的东西。

各种依赖注入灵活使用。个人觉得后三种都不错。