重構學習筆記(4)-大話重構第四章-保險索下的系統重構與自動化測試-筆記

重構

你不能沒有保險索

書中提及的針對軟體系統進行大範圍的、大幅度改動、就會造成毀滅性的結果,很多人不敢嘗試系統重構的原因。

在筆者實務上很常見系統的祖產是家常便飯,因為這種系統改動幅度風險相當高,即便採用【小步快跑】也一定有風險…

在書上提及的部分:這個保險索就是重構後的正確測試方法

不改變軟體的外部行為,功能層面來看以下為:

  • 重構前,使用者什麼樣的功能,重構應當提供同樣的功能
  • 重構前,輸入什麼樣的請求得到運算結果,重構後也應得到相同的結果

筆者曾經在改寫vb 6轉移.net API的時候就會將過程的部分搬移,先將邏輯搬移,確保程式的輸入,輸出是對的,在資料庫寫入的結果,舊程式跑過一次與新程式跑過,兩者比對,測試不同情境也沒問題的時候先上線在開始提取類別,提取介面,反覆的測試,讓後續接手維護成本沒這麼高,不改寫的狀況下,一堆人搶那唯一windows 98的環境去vb 6 debug…成本時間浪費相當高…而且也不好debug…

系統重構測試方法

  • 系統測試
  • 單元測試

系統測試:透過QTP進行簡單錄製、重構前透過需求確認系統的現有功能,根據現有功能設計測試案例,進入系統,,確保每個測試都通過,並錄製QTP。

註記:每完成一次重構就手動測試或執行QTP腳本,保證測試通過,若不通過,找尋問題並解決或者退到上次的測試版本,重構失敗。

不要過早寫自動化測試程式,它不一定節省時間,可能花費更多,初期重構可以採用手動測試或QTP錄製測試進行。
 

遺留系統共同的特點

  • 程式碼亂
  • 沒有清晰介面
  • 程式耦合度高
  • 相互依賴嚴重

撰寫自動化測試程式前

確保測試程式已經與web容器與其他設備驅動程式相關解耦合。

其實筆者解讀是例如像DB存取驅動或API及廠商提供SDK…等都要進行解耦合。

不合適自動化測試:就是存取資料庫程式,因為要跟程式要跟資料庫互動,結果跟資料庫有關,因為會可能改狀態或是環境問題等…跟資料庫存取部分就要進行Mock。

當進行【系統重構】就要把資料庫存取相關從業務邏輯抽離出來寫入到DAO,在C#來說應該是Repository Layer,最後Mock的Repository Layer不會存取資料庫。

總結:透過手動測試和OTP錄製方式進行重構,滿足自動化測試條件後,才能進行自動化測試。

自動化測試:關於Nunit

  1. 在Web Request,Reponse 和 Session和Cookie不太合適用Mock
  2. 自動化測試程式比較忌諱存取資料庫,因為下次執行不見得成功,資料庫不穩定結果會隨時變更,唯一可以做的就是Mock Data Layer(筆者常用的Repository層)

總結:並不是所有程式都合適加入自動化測試。

這個章節在講加入測試的部分,透過重構之後加入測試專案,用Nunit的套件,筆者是用.net 所以把範例有調整過以C#為主。

而根據三種情境,進行測試早安、午安、晚安的三種情境….

工具是採用NUnit

筆者依據這個Sample調整改寫我要的情境,依據前面的重構再加入時間的Library,接近Java的範例

  /// <summary>
    /// A utility about time.
    /// </summary>
    public class DateUtil
    {
        /// <summary>
        /// Get current hour of day.
        /// </summary>
        /// <param name="now"></param>
        /// <returns>
        /// current hour of day
        /// </returns>
        public int getHours(DateTime now)
        {
            Calendar calendar = new Calendar();
            calendar.setTime(now);
            return calendar.get(calendar.HOUR_OF_DAY);

        }
        /// <summary>
        /// Get current Datetime
        /// </summary>
        /// <param name="year"></param>
        /// <param name="month"></param>
        /// <param name="day"></param>
        /// <param name="hh"></param>
        /// <param name="mm"></param>
        /// <param name="ss"></param>
        /// <returns></returns>
        public DateTime createDate(int year, int month, int day, int hh, int mm, int ss)
        {
            return new DateTime(year, month, day, hh, mm, ss);
        }
    }
    /// <summary>
    /// The Refactoring's Hello-world program
    /// author eddie
    /// </summary>
    public class HelloWorld
    {
        /// <summary>
        /// Say hello to everyone.
        /// </summary>
        /// <param name="now"></param>
        /// <param name="user"></param>
        /// <returns>
        /// the words what to say
        /// /// </returns>
        public string sayHello(DateTime now, string user)
        {
            var dateUtil = new DateUtil();
            int hour = dateUtil.getHours(now);
            var greeting = new Greeting();

            if (hour >= 6 && hour < 12)
                return "Hi, " + user + "." + "Good morning!";
            else if (hour > 12 && hour < 19)
                return "Hi, " + user + "." + "Good afternoon!";
            else
                  return "Hi, " + user + "." + "Good night!";
        }
    }

開始撰寫三個測試情境

    [TestFixture]
    public class HelloTest
    {
        public DateUtil dateUtil;
        public HelloWorld helloWorld;
        [SetUp]
        public void Setup()
        {
            dateUtil = new DateUtil();
            helloWorld = new HelloWorld();
        }
        [Test]
        public void testSayHelloInTheMorning()
        {
            DateTime now = dateUtil.createDate(2013, 9, 7, 9, 23, 11);
            string user = "張三";
            string result = "";
            result = helloWorld.sayHello(now, user);
            Assert.AreEqual("Hi, 張三.Good morning!",result);
        }
        [Test]
        public void testSayHelloInTheAfternoon()
        {
            DateTime now = new DateUtil().createDate(2013, 9, 7, 15, 7, 11);
            string user = "張三";
            string result = "";
            result = helloWorld.sayHello(now, user);
            Assert.AreEqual("Hi, 張三.Good afternoon!", result);
        }

        [Test]
        public void testSayHelloInTheAtNight()
        {
            DateTime now = new DateUtil().createDate(2013, 9, 7, 21, 30, 11);
            string user = "張三";
            string result = "";
            result = helloWorld.sayHello(now, user);
            Assert.AreEqual("Hi, 張三.Good night!", result);
        }
    }

加入測試之後,接者使用者出現需求變化要編寫所謂的Happy New Year、9月份暑假

接者就要開始跑測試,為什麼測試會出錯,是因為需求已經出現變化,所以必須要為每一個程式編寫測試案例,讓他全部Pass

總結:從這個例子的演練過來會變成以下的步驟是

  1. 第一步:調整程式順序、追加註解、進行分段、並修改相關變數與命名,以利閱讀,因為此次加入測試程式之後,可以編譯,雖然程式內部有調整、但是外部沒變化、重構成功可以進行後續工作。
  2. 第二步:提取方法在SayHello()分別提出getFirstGreeting()和getSecondGreeting()和getHour()、在執行測試程式。
  3. 第三步:提取類別,分別在getFirstGreeting()和getSecondGreeting()形成GreetingToUser和GreetingAboutTime完成之後、測試通過。
  4. 第四步:當我們需求出現變化,還要依據不同日期判斷是否是節日狀況,我們採用兩頂帽子進行開發【首先先不引入新的需求,先修改原程式,適應新需求,提煉出DateUtil,增加getHour(),getMonth,getDate(),完成重構後測試通過。

當如果其中一個測試程式沒辦法pass,就得調整測試程式,為每一個情境寫測試,讓測試通過。

關於兩頂帽子:https://www.ithome.com.tw/voice/115057

 

老E隨手寫