[VS2010] UnitTest 其實沒那麼困難
很多情況下談到UnitTest時,我們常會聽到一些原因而導致沒有很好的一個方式
來進行UnitTest,更多數的情況下我們大都依賴人工的方式進行測試,前些時間
對一些同事分享了一個很簡單的實際案例,順便稍稍整理一下PO上來分享
在UnitTest時可能會有幾個原因,導致我們無法很easy to UnitTest
(1)程式碼跟資料庫很緊密的結合,每次要進行測試時總要花很多功夫把資料先準備好
(2)程式每一次修改後,沒有可以重覆利用的測試程式碼,只好依賴人工一一再測試
(3)人為的測試無法保證每一次的測試都完整檢測
(4)測試的時間總是比開發(修改)程式來的多,太累了
(5)系統架構早就已經存在了,程式碼大多是全寫在一起,現行情況無法做大翻修
(6)……
但……真的是如此我們就無法很easy to UnitTest嗎?
UnitTest的意義,個人認為主要是針對Business Logic的部份,那麼對於UI及DataAccess
的部份可以暫時放一邊去,也就是UnitTest的部份是要確保我們的Business Logic是正確
的,所以如果我們有這樣的前題認知時,在撰寫程式時可以把Business Logic的部份盡可能
與DataAccess及UI Control的部份切開,再配合VS2010的UnitTest功能,一切就會很easy
,以下直接以一個簡單的情境案例來演示說明
系統假設:
現行系統的架構沒有很明顯的分層切割,是傳統Web form的開發方式,Business Logic
及DataAccess大都是在某個事件中一併處理
情境案例:
有個新的功能要加入現在的系統中,此功能主要是要計算員工的保險費,保險
費的計算邏輯是以員工的薪資*x%,而x%是以年齡而有不同
a.年齡<=40,x%=1%
b.年齡>40 and <=50,x%=1.5%
c.年齡>50 ,x%=2%
(1)我們常見的程式寫法,可能會是如下
using (SqlConnection conn = new SqlConnection(System.Web.Configuration.WebConfigurationManager.ConnectionStrings["DBconnStr"].ToString()))
{
conn.Open();
using (SqlCommand command = new SqlCommand())
{
command.Connection = conn;
//找要計算的資料
command.CommandText = "select empno,age,salary from employee where ....";
IDataReader idr = command.ExecuteReader();
while (idr.Read())
{
//年齡<=40,x%=1%
if (int.Parse(idr["age"].ToString()) <= int.Parse("40"))
{
//計算保費 salary*1%
}
//年齡>40 and <=50,x%=1.5%
if ((int.Parse(idr["age"].ToString()) > int.Parse("40")) && (int.Parse(idr["age"].ToString()) >= int.Parse("50")))
{
//計算保費 salary*1.5%
}
//年齡>50 ,x%=2%
if (int.Parse(idr["age"].ToString()) > int.Parse("50"))
{
//計算保費 salary*2%
}
//insert 保費資料至資料庫
}
}
conn.Close();
}
這樣的code有幾個缺點
-
Business Logic要的資料來源跟資料庫緊密結合,要測試必需要準備資料庫
,但每次都要重新init一個新的db 不是件簡單易做的事 -
客戶日後要求畫面加個資料計算的篩選條件,這隻aspx要調整到,一有修改
就有可能不小心改到Business Logic,但加篩選條件這件事根本與Business Logic
無關
(2)轉個寫法讓你可以簡單擁抱UnitTest
我們先想一下,這個需求本身的Business Logic主要是
- 保費=員工的薪資*x%,而x%來自於以下規則所決定
- a.年齡<=40,x%=1%
- b.年齡>40 and <=50,x%=1.5%
- c.年齡>50 ,x%=2%
所以資料怎麼來及計算完要做什麼事,跟計算保費這件事沒什麼太大的關係,所以我們要
做的就是讓這個Business Logic夠自私只管自己的職責,也就是計算這件事就好以下是改
良後的寫法
(A)設計一個employee class,其雍有empno(員編),age(年齡),salary(薪資)三個屬性
(B)設計一個保費計算的class:InsuranceAmount
Calculate提供給外界AP呼叫使用的Method,本身只處理把計算後結果寫入資料庫的事情
,真正的Business Logic則由另一個private 的Method來處理
public void Calculate(List<Employee> emplist)
{
foreach (var item in emplist)
{
int amount = 0;
amount = CalculateLogic(item);
using (SqlCommand command = new SqlCommand())
{
//依結算結果寫入db
}
}
}
CalculateLogic真正的Business Logic Method
private int CalculateLogic(Employee emp)
{
int amount = 0;
if (emp.Age <= 40)
{
amount = int.Parse(Math.Round(emp.Salary * 0.01, 0, MidpointRounding.AwayFromZero).ToString());
}
else if ((emp.Age > 40) && (emp.Age <= 50))
{
amount = int.Parse(Math.Round(emp.Salary * 0.015, 0, MidpointRounding.AwayFromZero).ToString());
}
else
{
amount = int.Parse(Math.Round(emp.Salary * 0.02, 0, MidpointRounding.AwayFromZero).ToString());
}
return amount;
}
(C) 而原本的ASPX,只需處理有關UI的部份,以及負責把要計算的資料以List<T>的型式
傳給保費計算的class
List<VS2010Demo.BLL.Employee> emplist = new List<BLL.Employee>();
using (SqlConnection conn = new SqlConnection(System.Web.Configuration.WebConfigurationManager.ConnectionStrings["DBconnStr"].ToString()))
{
conn.Open();
using (SqlCommand command = new SqlCommand())
{
command.Connection = conn;
//找要計算的資料
command.CommandText = "select empno from employee where ....";
IDataReader idr = command.ExecuteReader();
while (idr.Read())
{
VS2010Demo.BLL.Employee emp = new BLL.Employee(idr["empno"].ToString());
emplist.Add(emp);
}
//Call計算的Business Logic Class
VS2010Demo.BLL.InsuranceAmount insamt = new BLL.InsuranceAmount();
insamt.Calculate(emplist);
}
conn.Close();
}
經過這樣的調整之後,已經把最主要的Business Logic的部份抽離了跟UI有關的部份,
也就是未來客戶日後要求畫面加個資料計算的篩選條件 ,基本上都跟InsuranceAmount
Class無關,AP面只需要負責準備好List<Employee>傳給計費Class method,而計費的
Class也不去管外界怎麼準備那些資料,只管接受到什麼樣的資料,就做哪些運算即可,
因此也不用担心不小心去動到了計費Logic。
另外在InsuranceAmount裡雖然依現在系統架構的關係,並沒有把DataAccess的部份
給切開來,但內部我們依然做了點小調整(參考上面B的程式碼),把主要的Business Logic
抽出來,形成一個private method( CalculateLogic ),而這個private method就完全跟
DataAccess無關了,接著我們就可以利用VS2010 UnitTest的功能,針對CalculateLogic
這個Business Logic method撰寫測試程式碼,而且相當容易。
(D)於CalculateLogic method中/右鍵/建立單位測試
(E)接著撰寫測試案例程式碼,針對不同的情況條件,可以分別撰寫不同的測試案例
,而且由於資料來源我們在設計是以employee class做為來源,因此在撰寫測試案
例時,我們並不用依賴資料庫,可以隨著我們想要的測試案例new 出我們想要的employee
data
[TestMethod()]
[DeploymentItem("VS2010Demo.dll")]
public void 年齡小於等於40且薪資28000()
{
InsuranceAmount_Accessor target = new InsuranceAmount_Accessor(); // TODO: 初始化為適當值
Employee emp = new Employee(); // TODO: 初始化為適當值
emp.Age = 40;
emp.Salary = 28000;
int expected = 280; // TODO: 初始化為適當值
int actual;
actual = target.CalculateLogic(emp);
Assert.AreEqual(expected, actual);
//Assert.Inconclusive("驗證這個測試方法的正確性。");
}
經過這個情境的演示,是不是開始覺得Unit Test其實可以很easy啊,在這個案例中
雖然系統架構無法大調整,沒辦法完全把UI、DataAccess、Business Logic分開,
但這並不代表我們就不能進行Unit Test測試,只要我們做個小調整依然可以很easy
to Unit Test的,並且這些測試程式碼可以重覆運用的,在反覆測試時可以省下不少
的資料準備時間及人工成本。
PS:本篇情境案例在於模擬一個現實情境下,可能會遇到的情況,至於案例中的Class
Design是否合適在此就不多做著墨了。
By No.18