iT邦幫忙

DAY 10
1

30天快速上手TDD系列 第 10

[Day 10]Refactoring 起手式 - 建立測試

上一篇文章中,介紹了如何透過一些靜態程式碼分析的工具,搭配品質指標的門檻,來快速找到系統中需要重構的程式。

也稍微的介紹了,重構目標的程式基本功能與樣式。

這一篇文章則要開始進行重構了,保證每一步都相當簡單,大家都可以跟著做到。

上一篇文章:[Day 9]Refactoring legacy code簡介
本系列文章專區
@前言
.aspx的程式碼:

<%@ Page Title="" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeFile="Product_v0.aspx.cs" Inherits="Product_v0" %>

<asp:Content ID="Content1" ContentPlaceHolderID="HeadContent" runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="Server">
    <div>
        <fieldset>
            <legend>商品資訊</legend>
            <table style="width: 100%;">
                <tr>
                    <td>
                        商品名稱
                    </td>
                    <td>
                        <asp:TextBox ID="txtProductName" runat="server"></asp:TextBox>
                        <asp:RequiredFieldValidator ID="RequiredFieldValidator2" runat="server" ErrorMessage="請輸入商品名稱"
                            ControlToValidate="txtProductName"></asp:RequiredFieldValidator>
                    </td>
                </tr>
                <tr>
                    <td>
                        重量
                    </td>
                    <td>
                        <asp:TextBox ID="txtProductWeight" runat="server"></asp:TextBox>
                        <asp:RequiredFieldValidator ID="RequiredFieldValidator3" runat="server" ErrorMessage="請輸入商品重量"
                            ControlToValidate="txtProductWeight"></asp:RequiredFieldValidator>
                    </td>
                </tr>
                <tr>
                    <td>
                        長
                    </td>
                    <td>
                        <asp:TextBox ID="txtProductLength" runat="server"></asp:TextBox>
                        <asp:RequiredFieldValidator ID="RequiredFieldValidator4" runat="server" ErrorMessage="請輸入商品長度"
                            ControlToValidate="txtProductLength"></asp:RequiredFieldValidator>
                    </td>
                </tr>
                <tr>
                    <td>
                        寬
                    </td>
                    <td>
                        <asp:TextBox ID="txtProductWidth" runat="server"></asp:TextBox>
                        <asp:RequiredFieldValidator ID="RequiredFieldValidator5" runat="server" ErrorMessage="請輸入商品寬度"
                            ControlToValidate="txtProductWidth"></asp:RequiredFieldValidator>
                    </td>
                </tr>
                <tr>
                    <td>
                        高
                    </td>
                    <td>
                        <asp:TextBox ID="txtProductHeight" runat="server"></asp:TextBox>
                        <asp:RequiredFieldValidator ID="RequiredFieldValidator6" runat="server" ErrorMessage="請輸入商品高度"
                            ControlToValidate="txtProductHeight"></asp:RequiredFieldValidator>
                    </td>
                </tr>
                <tr>
                    <td>
                        是否需低溫冷藏
                    </td>
                    <td>
                        <asp:RadioButtonList ID="rdoNeedCool" runat="server" RepeatDirection="Horizontal">
                            <asp:ListItem Value="1">是</asp:ListItem>
                            <asp:ListItem Value="0">否</asp:ListItem>
                        </asp:RadioButtonList>
                        <asp:RequiredFieldValidator ID="RequiredFieldValidator7" runat="server" ErrorMessage="請輸入是否需低溫冷藏" ControlToValidate="rdoNeedCool"></asp:RequiredFieldValidator>
                    </td>
                </tr>
                <tr>
                    <td>
                        物流商
                    </td>
                    <td>
                        <asp:DropDownList ID="drpCompany" runat="server">
                            <asp:ListItem>請選擇</asp:ListItem>
                            <asp:ListItem Value="1">黑貓</asp:ListItem>
                            <asp:ListItem Value="2">新竹貨運</asp:ListItem>
                            <asp:ListItem Value="3">郵局</asp:ListItem>
                        </asp:DropDownList>
                        <asp:RequiredFieldValidator ID="RequiredFieldValidator1" ControlToValidate="drpCompany"
                            InitialValue="請選擇" runat="server" ErrorMessage="請選擇物流商"></asp:RequiredFieldValidator>
                    </td>
                </tr>
            </table>
            <asp:Button ID="btnCalculate" runat="server" Text="計算運費" 
                onclick="btnCalculate_Click" />
        </fieldset>
    </div>
    <div>
        <fieldset>
            <legend>結果</legend>物流商:<asp:Label ID="lblCompany" runat="server"></asp:Label>
            <br />
            運費:<asp:Label ID="lblCharge" runat="server"></asp:Label>
        </fieldset>
    </div>
</asp:Content>

網頁畫面如下:

@找到目標後,如何開始重構
重構的循環有幾個階段,分別為綠燈、重構、紅燈、填入。如下圖所示:

當我們想要進行『重構』的動作:

就應該先進行『綠燈』的前置作業:

@重構起手式:口說無憑、錄影存證
要記住,現況的程式碼,雖然彷彿一坨垃圾,但他是可以執行出正確結果的垃圾。寫得再好、再完美的程式,如果無法執行出正確的結果,那也沒啥價值可言。

既然,我們要進行重構,重構的意義就在於:『不改變系統外在行為的條件下,改善系統內部的品質』,改程式很簡單,要確保只影響到我們改的程式,要確保原本的行為沒有改變,這個前提要比改程式重要得多。

所以,這邊透過Selenium IDE,先來幫助我們記錄下來現在可以執行出正確結果的行為。

時間,應該浪費在美好的事物上,而不是每次修改完程式,都還要手動去key in一堆沒意義的資料。用最少的effort,達到自動化的效果。Selenium的使用介紹,請見[Day 8]Integration Testing & Web UI Testing

@步驟
確保現在程式可以執行出正確的結果後,開始錄製腳本:

  1. 打開Selenium IDE後,按下錄製
  2. 輸入商品的資訊
  3. 選擇物流商
  4. 按下計算按鈕
  5. 將物流商名稱與運費結果記錄下來,並加入驗證項目

錄製的腳本如下圖所示:

錄製過程中,請記得要在適當的步驟,加入verify的項目,確保到哪一個步驟時,應該有對應的預期結果。

以這邊的例子來說,就是「當選完物流商,重新點選計算運費時,我們會去驗證物流商的名稱,以及運費的結果,是否符合預期。」

看一下測試腳本,大概就知道測試案例進行了哪些動作,如下圖:

這裡,暫時不需要將腳本轉換成C#的測試程式。因為我們的目的,只是確保重構完成後,原本可以正常執行的程式,仍然可以符合預期般正常執行。

@小結
TDD中循環的三大步驟:紅燈、綠燈、重構。

當切入點為重構時,首先就是要確認系統可以正常運作,接下來建立測試,這個測試建立完成後,應該可以通過測試,也就是第一個綠燈。

即使legacy的程式碼,是屬於物件直接相依,條件判斷邏輯可能也相當複雜或醜陋,沒關係,我們的第一步,就是確保最後的執行結果,仍然符合使用者的需求。

還記得嗎?越抽象、越上層的測試,基本上花的成本越小,但異動的頻率可能也會越高。Selenium在這個例子中,就可以發揮效益比最大的功效。因為我們花的成本,只有再操作一次系統而已。

當建立好了這個可以迅速、可重複、可自動執行的Web UI測試後,就可以當作是進入了TDD循環的綠燈階段。

接下來不管改了什麼程式,動了什麼手腳,即便是傷筋動骨,也可以確保最後產出結果,仍符合使用者預期。

更棒的是,基本上不會發生程式不小心改錯而不知情的情況,如果沒有這重要的起手式,就沒人可以保證修改完的程式是對的。

有了這一層最終的保護,也是我們最終的目的,就比較不會發生程式要上線後才由使用者發現問題的情況。如果,真有這樣的情況發生,代表測試案例不夠周全、完整,要做的應該是增加測試案例,並且設法通過測試案例。

還記得嗎?「程式碼不是寫給developer爽的,程式碼存在的目的,是為了滿足使用者的需求」,而「測試案例,就代表著使用者的需求有沒被涵蓋與驗證完成」

最後,以一句話總結:「重構的第一步,請先建立測試

@補充
當懂得如何重構,也學會如何作單元測試的朋友,在實務上會碰到的第一個問題,就是一個矛盾的問題。

  1. 程式碼要避免直接相依,才有可測試性。
  2. 程式碼要重構,才能解開直接相依的耦合性。
  3. 要先建立測試,才能重構。

這不就一環咬著一環嗎? 毆飛

所以,前面才花了這麼多篇文章來介紹不同層級的測試。

這三步的矛盾點,在於「單元測試」得程式碼具備可測試性,也就是得物件獨立,得物件不直接相依。但沒有測試,又不給重構。

因此,只需要再建立一層更高層級的測試,成本低,穩定性也低(但即使是用過一次即丟,也還是有其價值所在)透過UI的迴歸測試、整合測試,來保護最終結果符合使用者預期,接下來只要小幅度地開始進行重構,直到物件職責分開、相依性分開後,只需要接著建立相關物件的單元測試,那麼整段程式碼的重構循環,也就告一段落了。

如果讀者朋友們,眼前碰到的legacy system refactoring難題就是這個矛盾,just try it! 您也可以很輕鬆地就解決這個矛盾點唷。
讚


上一篇
[Day 9]Refactoring legacy code簡介
下一篇
[Day 11]Refactoring - 讓程式碼說話
系列文
30天快速上手TDD31

2 則留言

0
pajace2001
iT邦研究生 1 級 ‧ 2012-10-18 01:39:35

沙發
先搶沙發,再來好好欣賞....

0
pajace2001
iT邦研究生 1 級 ‧ 2012-10-18 11:03:17

hatelove提到:
這不就一環咬著一環嗎?

有的時候真的是這樣!! 一個咬一個! 所以到最後就會變成,能測就測~真的沒辦法~只好先放棄了XD

就是91 iT邦研究生 4 級 ‧ 2012-10-18 11:22:06 檢舉

是啊,這是自己碰到的實務經驗,左手卡右手的情況。

所以希望分享一下這個繞道的簡單解決方式,能突破大家的盲點。

我要留言

立即登入留言