关于老旧代码补充单元测试的接缝处理(如何通过依赖注入解决代码的依赖问题)

上次我们说到了可以利用单元测试辅助我们进行代码的重构。众所周知,单元测试的最佳切入点,是在写代码之前。有很多老旧代码可能是不太适合单元测试的直接插入的。所以上次的讨论遗留了一个问题:有些方法很长,做了很多事情,甚至没有返回值,我怎么把这些方法分解开,然后套上单元测试?我们把这个问题换一个说法:如何将一个单元测试覆盖到一个老旧的,复杂的,冗长的类或方法上。我在上次的讨论结尾,留了一个名词:接缝,不知大家是否还有映像。
      接缝的定义为:
程序中的一些特殊的点,在这些点上你无需做任何修改,就可以达到改动程序行为的目的。
接缝的定义比较抽象,不太好理解。我们可以把接缝简单的理解为依赖点。通过事先找到依赖点,并采取一定的方式解除依赖,就能够给程序带来可测试性,进而改善代码质量,使其具有可重用性,与可扩展性——尤其针对遗留代码而言。那么我们把开头的问题再换一个说法:如何给老旧的,复杂的,冗长的类或方法解除依赖?在这里,我们不得不提到面向对象设计的一个重要原则:依赖倒置原则,以及一个常用的技巧:控制反转。
什么是依赖倒置?
1. 上层模块不应该依赖底层模块,它们都应该依赖于抽象。
2. 抽象不应该依赖于细节,细节应该依赖于抽象。
用通俗的大白话说:就是在涉及到依赖的时候,尽可能考虑接口和抽象类。举几个例子,先来看一段普通的代码(以下代码删除了大量无需说明的业务逻辑):
public ActionResult downSelUpFilesListRar(string strCaseNo, string strNodeArr, string FCFJFlag = "0")
{
    //调用数据库连接类
    GS.DataBase.Oracle Db = new GS.DataBase.Oracle("Password=123;Data Source=orcl;User ID=234;");
    //........
    DataSet ds = null;
    string[] strArr = strNodeArr.Split('$');
    foreach (string str in strArr)
    {
        string strSql = "select * from dual";
        //.......
        ds = Db.GetDataSet(strSql);
        if (ds != null && ds.Tables.Count > 0 && ds.Tables[0].Rows.Count > 0)
        {
            //......
            try
            {
                //.....
                Ftp ftp = new Ftp(strFtpPath);
                int ftpResult = ftp.DownloadFtp(strDirPath + "\\" + ds.Tables[0].Rows[0][0].ToString(), ds.Tables[0].Rows[0]["FILEPATH"].ToString());
                if (ftpResult < 0)
                {
                    throw new ArgumentException("从FTP下载附件出现异常:" + ftpResult);
                }
                //........
            }
            catch
            {
                throw;
            }
            finally
            {
                if (fw != null)
                {
                    fw.Close();
                }
            }
        }
    }

    return Json(strRarPath);
}

  这段代码运行起来没有问题,但是如果我们想要加上单元测试的话,就会遇到一些麻烦。它在方法体里面直接实例化了两个依赖:数据库控制类和FTP控制类,而这两个类都属于底层模块,这个代码本身属于上层模块,所以它违反了上层模块不应该依赖底层模块的依赖倒置原则。进而导致如果要更换数据库或FTP导致数据库控制类和FTP控制类发生变化后,不得不回过头来编辑这个方法。当然这不是我们今天要讲的重点。我们的重点在于,这个方法无法进行单元测试,因为我们在进行单元测试的时候,不可能原样去准备一个一摸一样的数据库服务器和FTP服务器。直接对这个方法进行单元测试,就会卡在初始化数据库类和FTP类的代码语句上。

回到今天开始的问题,在这段代码中,什么是接缝?
数据库控制类GS.DataBase.Oracle和FTP控制类Ftp就是接缝(依赖点)。我们接下来将代码稍作修改,通过控制反转的操作,使其符合依赖倒置。
public ActionResult downSelUpFilesListRar(GS.DataBase.IDbAccess iDb,IFtp iftp,string strCaseNo, string strNodeArr, string FCFJFlag = "0")
{
    //........
    DataSet ds = null;
    string[] strArr = strNodeArr.Split('$');
    foreach (string str in strArr)
    {
        string strSql = "select * from dual";
        //.......
        ds = iDb.GetDataSet(strSql);
        if (ds != null && ds.Tables.Count > 0 && ds.Tables[0].Rows.Count > 0)
        {
            //......
            try
            {
                //.....
                int ftpResult = iftp.DownloadFtp(strDirPath + "\\" + ds.Tables[0].Rows[0][0].ToString(), ds.Tables[0].Rows[0]["FILEPATH"].ToString());
                if (ftpResult < 0)
                {
                    throw new ArgumentException("从FTP下载附件出现异常:" + ftpResult);
                }
                //........
            }
            catch
            {
                throw;
            }
            finally
            {
                if (fw != null)
                {
                    fw.Close();
                }
            }
        }
    }

    return Json(strRarPath);
}

  我们做了什么操作?

  首先,我们将数据库控制类GS.DataBase.Oracle和FTP控制类Ftp抽象出了接口,这样以后不论数据库更换成Sqlserver还是Mysql,ftp不论是用wendows的还是liux的,我们都不需要再改动我们的代码,因为我们使用的是接口,其他的具体操作类都要实现这个接口。

其次,我们将GS.DataBase.IDbAccess接口和IFTP接口作为参数传入了,这样我们将依赖点的实例化操作推到了函数之外。
这里引出另外两个重要的概念:控制反转,依赖注入
什么是控制反转?
控制反转是一种新的设计模式,它对上层模块与底层模块进行了更进一步的解耦。控制反转的意思是反转了上层模块对于底层模块的依赖控制。
什么是依赖注入?
依赖注入其实是一种控制反转的手段,简单的表述就是:不再自己实例化依赖,而是要外部模块创建好,在适当的时候注入进来为己所用。
依赖注入有什么好处?回到我们今天的主题:为老旧代码添加单元测试。当代码的依赖点都通过依赖注入进行初始化的时候,我们就能够在单元测试中初始化的时候进行MOKE操作了。关于MOKE我们上次讲过,是在单元测试的时候,通过编写一些实现了待测试方法或类要求的接口或抽象类的测试模拟类。譬如在上述例子中,我们就可以编写一个实现了GS.DataBase.IDbAccess接口的MoqGS类,但是这个类的GetDataSet方法并不是真的到数据库中去查找相关数据,而是从文本中返回一些固定的测试数据。当然,我们还可以使用一些通用的框架,譬如Moq来帮我们快速实现一些moke类。
回到依赖注入,一般来说,常用的依赖注入方法有如下三种: 
  1. 构造函数中注入 
  2. setter 方式注入 
  3. 接口注入
其中,用得最多的要数构造函数注入和setter 方法注入。
构造函数注入:
即被注入对象可以通过在其构造方法中声明依赖对象的参数列表,让外部(通常是IOC容器)知道它需要哪些依赖对象,然后IOC容器会检查被注入对象的构造方法,取得其所需要的依赖对象列表,进而为其注入相应对象。
优点:在对象一开始创建的时候就确定好了依赖。 
缺点:后期无法更改依赖
public class downSelUpFiles
{
    private GS.DataBase.IDbAccess _idb;
    private IFtp _iftp;

    public downSelUpFiles(GS.DataBase.IDbAccess idb,IFtp iftp)
    {
        this._idb = idb;
        this._iftp = iftp;
        downSelUpFilesListRar(this._idb, this._iftp, "", "", "");
    }
    
    private ActionResult downSelUpFilesListRar(string strCaseNo, string strNodeArr, string FCFJFlag = "0")
    {
        //........
        DataSet ds = null;
        string[] strArr = strNodeArr.Split('$');
        foreach (string str in strArr)
        {
            string strSql = "select * from dual";
            //.......
            ds = this._idb.GetDataSet(strSql);
            if (ds != null && ds.Tables.Count > 0 && ds.Tables[0].Rows.Count > 0)
            {
                //......
                try
                {
                    //.....
                    int ftpResult = this._iftp.DownloadFtp(strDirPath + "\\" + ds.Tables[0].Rows[0][0].ToString(), ds.Tables[0].Rows[0]["FILEPATH"].ToString());
                    if (ftpResult < 0)
                    {
                        throw new ArgumentException("从FTP下载附件出现异常:" + ftpResult);
                    }
                    //........
                }
                catch
                {
                    throw;
                }
                finally
                {
                    if (fw != null)
                    {
                        fw.Close();
                    }
                }
            }
        }

        return Json(strRarPath);
    }
}

  setter 方式注入:

即当前对象只需要为其依赖对象所对应的属性添加setter方法,IOC容器通过此setter方法将相应的依赖对象设置到被注入对象的方式即setter方法注入。
优点:对象在运行过程中可以灵活地更改依赖。 
缺点:对象运行时,可能会存在依赖项为 null 的情况,所以需要检测依赖项的状态。
其实我们的第二个例子就是一个简化版的setter注入方式。下面我们写一个标准版的setter注入。
 
public class downSelUpFiles
{
    private GS.DataBase.IDbAccess _idb;
    private IFtp _iftp;

    public downSelUpFiles()
    {

    }

    public void setGS(GS.DataBase.IDbAccess iDb)
    {
        this._idb = idb;
    }

    public void setFtp(IFtp iftp)
    {
        this._iftp = iftp;
    }

    public ActionResult downSelUpFilesListRar(string strCaseNo, string strNodeArr, string FCFJFlag = "0")
    {
        //........
        DataSet ds = null;
        string[] strArr = strNodeArr.Split('$');
        foreach (string str in strArr)
        {
            string strSql = "select * from dual";
            //.......
            ds = this._idb.GetDataSet(strSql);
            if (ds != null && ds.Tables.Count > 0 && ds.Tables[0].Rows.Count > 0)
            {
                //......
                try
                {
                    //.....
                    int ftpResult = this._iftp.DownloadFtp(strDirPath + "\\" + ds.Tables[0].Rows[0][0].ToString(), ds.Tables[0].Rows[0]["FILEPATH"].ToString());
                    if (ftpResult < 0)
                    {
                        throw new ArgumentException("从FTP下载附件出现异常:" + ftpResult);
                    }
                    //........
                }
                catch
                {
                    throw;
                }
                finally
                {
                    if (fw != null)
                    {
                        fw.Close();
                    }
                }
            }
        }

        return Json(strRarPath);
    }
}

  其他的解除依赖的手段还有:配置文件与反射技术,表驱动法等,他们各有优缺点,因为应用相对较少,在此不做赘述。

上述例子其实相对简单,真实生产编码环境中,有大量的比例子复杂得多的代码依赖和互相调用,在做接缝处理的时候,其实要千万小心,仔细,防止破坏原有的代码结构和正确性。
当我们将大部分的接缝(依赖点)处理之后,我们就能够为这些遗留代码穿上单元测试的外套,在单元测试的保护下,我们就可以开始愉快的进行代码修改和重构了。下个月,我将带大家走进代码重构的世界。

猜你喜欢

转载自www.cnblogs.com/wenpeng/p/10179962.html
今日推荐