[VS2010] UnitTest 其實沒那麼困難

  • 5754
  • 0

[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(薪資)三個屬性

image

 

(B)設計一個保費計算的class:InsuranceAmount

image

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中/右鍵/建立單位測試

image

 

image

 

(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("驗證這個測試方法的正確性。");
}

image

 

經過這個情境的演示,是不是開始覺得Unit Test其實可以很easy啊,在這個案例中

雖然系統架構無法大調整,沒辦法完全把UI、DataAccess、Business Logic分開,

但這並不代表我們就不能進行Unit Test測試,只要我們做個小調整依然可以很easy

to Unit Test的,並且這些測試程式碼可以重覆運用的,在反覆測試時可以省下不少

的資料準備時間及人工成本。

 

PS:本篇情境案例在於模擬一個現實情境下,可能會遇到的情況,至於案例中的Class

Design是否合適在此就不多做著墨了。

 

 

 

 

 

 

 

 

 

 

 

 

 

若本文對您有所幫助,歡迎轉貼,但請在加註【轉貼】及來源出處,並在附上本篇的超連結,感恩您的配合囉。

By No.18