本文透過開發一個資料物件與中介資料的函式庫為例,說明規格書、測試案例及單元測試如何被長出來。
決定要提供的服務
首先確定函式庫功能:
「提供資料物件與中介資料的相互轉換功能。」
要能相互轉換所以定義 Export 以及 Loading 提供「匯出」及「載入」兩項功能,讓客戶端能夠將資訊在資料物件與中介資料之間轉換。
先看看對 Export 功能的定義:
「提供將傳入資料物件的屬性導出為中介資料型式」
為了能夠具備一般通用性質,則資料物件不能限定特定類別或介面,然後中介資料類型也應該要能進行擴展。基於此,所以就發展出下面的介面:
internal interface IDataAdapter<TTransitData>
{
TTransitData Export<TModel>(TModel pi_objSource);
}
接著,透過類別 DataAdapter 實做 string 型別中介資料的 IDataAdapter 如下,這裡實做時並非採用明確介面實做,因為希望客戶端使用時不需要知道 IDataAdapter<TTransitData> 介面且不需要刻意的指定中介資料型別就可以使用。
public class DataAdapter():IDataAdatpter<string>
{
public string Export<TModel>(TModel pi_objSoruce)
{
...
}
}
如此一來,客戶端可以採用下述的使用方式來匯出資料物件:
var objAdapter = new DataAdapter();
var objSource = new TempDataObject();
var sTansitData = objAdapter.Export(objSource);
但是在擴展不同中介資料,例如 XElement 時,會造成多型錯誤,因為多型的簽名檔是不考慮輸出型別,所以下述的類別是無法通過編譯。
public class DataAdapter():IDataAdatpter<string>, IDataAdapter<XElement>
{
public string Export<TModel>(TModel pi_objSoruce)
{
...
}
public XElement Export<TModel>(TModel pi_objSoruce)
{
...
}
}
為此,調整使用者介面如下:
internal interface IDataAdapter<TTransitData>
{
bool Export<TModel>(TModel pi_objSource, ref TTransitData pi_objTransitData);
}
將中介資料改為簽名檔的成分,回傳值則改為匯出過程是否正常。對應此調整的類別則為:
public class DataAdapter():IDataAdatpter<string>, IDataAdapter<XElement>
{
public bool Export<TModel>(TModel pi_objSoruce, ref string pi_objTransitData)
{
...
}
public bool Export<TModel>(TModel pi_objSoruce, ref XElement pi_objTransitData)
{
...
}
}
跨出第一步
我將前述的設計透過 SpecFlow 進行描述,建立了兩個 Feature 檔案,分別代表 string 及 XElement 兩中介資料型式,然後開始建立對應的情境。
Feature: DataAdapterSpec_ExportToString
提供資料物件匯出為字串( String )功能。
第一個情境:
傳入空物件時回傳空字串
Feature: DataAdapterSpec_ExportToString
提供資料物件匯出為字串( String )功能。
Scenario: 傳入空物件時回傳空字串
When 要求轉換空物件
Then 回傳值為成功
Then 取回空字串
在這裡指定傳入空物件,並不會造成回傳值為失敗,而是得到一個空字串。
撰寫對應測試程式:
[When(@"要求轉換空物件")]
public void When要求轉換空物件()
{
var objAdapter = new DataAdapter();
var objSoruce = default(TempDataObject);
var bExportResult = objAdapter.Export<TempDataObject>(objSource, out string sExportResult);
ScenarioContext.Current.Set<bool>(bExportResult, "Result");
ScenarioContext.Current.Set<string>(sExportResult, "Actual");
}
北時執行測試必然會出錯。因為目前產品碼還尚未實做內容,會直接擲出預設例外,所以實做產品程式如下,讓測試通過,是的,就只是為了讓測案例通過:
public class DataAdapter():IDataAdatpter<string>, IDataAdapter<XElement>
{
public bool Export<TModel>(TModel pi_objSoruce, ref string pi_objTransitData)
{
pi_objTransitData = string.Empty;
return true;
}
public bool Export<TModel>(TModel pi_objSoruce, ref XElement pi_objTransitData)
{
...
}
}
接下來描述第二個情境:
傳入資料物件時回傳對應中介資料
Scenario: 傳入資料物件時回傳對應中介資料
When 要求轉換資料物件
| SGID |
| ABC-DEF-GHI |
Then 取回中介文字資料
"""
<Root>
<SGID><![CDATA[ABC-DEF-GHI]]></SGID>
</Root>
"""
在這裡新增了一個資料物件類別,具備 SGID 屬性,執行測試會失敗,因為目前的產品碼只是直接回傳空字串,而沒有任何的處理邏輯,因此,更新產品碼如下,讓測試通過,是的,就是透過寫死回傳結果讓測試案例可以通過:
public bool Export<TModel>(TModel pi_objSoruce, ref string pi_objTransitData)
{
var bReturn = false;
pi_objTransitData = string.Empty;
if (pi_objModel != null)
{
var objResult =
new XElement("Root",
new XElement("SGID",
new XCData("ABC-DEF-GHI")));
pi_objTransitData = objResult.ToString();
}
bReturn = true;
return bReturn;
}
重構
對於匯出資料物件為字串格式中介資料,目前的程式邏輯就是直接寫死對應測試案例,那是否還要建立其他案例來驗測它其實是寫死?其實不用,測試案例應該越少越好,因為很重要的一點:
測試案例及測試程式無法產出客戶價值。
測試案例的建立應該是採用具備代表性的,在某個特定的定義域下的只需要一個測試案例即可。以這個測試案例來說,就可以說是一般性操作,可以讓我們調整產品碼如下,透過 PropertyInfo 取得屬性值,然後建立中介資料。
public bool Export<TModel>(TModel pi_objSoruce, ref string pi_objTransitData)
{
var bReturn = false;
pi_objTransitData = string.Empty;
if (pi_objModel != null)
{
var objReturn = new XElement("Root");
foreach (PropertyInfo objProperty in typeof(TModel).GetProperties())
{
var sName = objProperty.Name;
var sValue = objProperty.GetValue(pi_objModel).ToString();
objReturn.Add(new XElement(sName, new XCData(sValue)));
}
pi_objTransitData = objReturn.ToString();
}
bReturn = true;
return bReturn;
}
接著可以建立其他案例來「說明」這個匯出方法的處理,例如,沒有指定屬性值時,匯出的中介資料如何表達、不同的屬性型別例如日期型別的表達或是未指定資料物件的屬性值的處理;但並非必要,可以在往後有遇到該情境時再逐一定義即可,也就是「小步進行」及「只寫剛好夠用的程式」。
結語
在這個實做中,是從產品碼的介面開始,然後再發展出規格及案例,在實務上很容易就一路將產品碼寫下去而忽略了規格、案例及測試的撰寫,當下總是覺得很理所當然,也沒有什麼值得寫測試之處。
所以應該從新增規格開始,透過 Feature 說明功能概念,利用 Scenario 透過定義域及值域的概念來表達功能內容,從 Feature 檔的自然語言的描述開始,透過 Step 檔的測試碼建立產品碼介面,再依此撰寫產品碼。