1. 程式人生 > >關於老舊程式碼補充單元測試的接縫處理(如何通過依賴注入解決程式碼的依賴問題)

關於老舊程式碼補充單元測試的接縫處理(如何通過依賴注入解決程式碼的依賴問題)

上次我們說到了可以利用單元測試輔助我們進行程式碼的重構。眾所周知,單元測試的最佳切入點,是在寫程式碼之前。有很多老舊程式碼可能是不太適合單元測試的直接插入的。所以上次的討論遺留了一個問題:有些方法很長,做了很多事情,甚至沒有返回值,我怎麼把這些方法分解開,然後套上單元測試?我們把這個問題換一個說法:如何將一個單元測試覆蓋到一個老舊的,複雜的,冗長的類或方法上。我在上次的討論結尾,留了一個名詞:接縫,不知大家是否還有映像。       接縫的定義為: 程式中的一些特殊的點,在這些點上你無需做任何修改,就可以達到改動程式行為的目的。
接縫的定義比較抽象,不太好理解。我們可以把接縫簡單的理解為依賴點。通過事先找到依賴點,並採取一定的方式解除依賴,就能夠給程式帶來可測試性,進而改善程式碼質量,使其具有可重用性,與可擴充套件性——尤其針對遺留程式碼而言。那麼我們把開頭的問題再換一個說法:如何給老舊的,複雜的,冗長的類或方法解除依賴?在這裡,我們不得不提到面向物件設計的一個重要原則:依賴倒置原則,以及一個常用的技巧:控制反轉。 什麼是依賴倒置? 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);
    }
}

  其他的解除依賴的手段還有:配置檔案與反射技術,表驅動法等,他們各有優缺點,因為應用相對較少,在此不做贅述。

上述例子其實相對簡單,真實生產編碼環境中,有大量的比例子複雜得多的程式碼依賴和互相呼叫,在做接縫處理的時候,其實要千萬小心,仔細,防止破壞原有的程式碼結構和正確性。 當我們將大部分的接縫(依賴點)處理之後,我們就能夠為這些遺留程式碼穿上單元測試的外套,在單元測試的保護下,我們就可以開始愉快的進行程式碼修改和重構了。下個月,我將帶大家走進程式碼重構的世界。