[Unit Test]單元測試簡介-Slide
前言
整理文件時發現一年前在部門分享的『單元測試簡介』投影片,就順便整理成文章,供朋友參考一下。
簡報分享
Demo說明
3A原則(Arrange, Act, Assert)
目標程式:
public class Operation
{
public int Add(int a, int b)
{
return a + b;
}
}
測試程式
/// <summary>
///Add 的測試
///</summary>
[TestMethod()]
public void AddTest()
{
//arrange
Operation target = new Operation();
int a = 1;
int b = 2;
int expected = 3;
int actual;
//act
actual = target.Add(a, b);
//assert
Assert.AreEqual(expected, actual);
}
Arrange指的是:
- 初始化測試目標類別,若為靜態方法則不需初始化
- 初始化測試目標類別所相依的介面instance,也就是所謂的stub/mock object
- 初始化測試目標類別上,要測試的方法,所需要使用到的參數
- 初始化預期結果
Act指的是:
- 呼叫測試目標類別上,要測試的方法
Assert指的是:
- 驗證預期結果與Act所得到的實際結果,是否符合
測試目標類別與外部類別直接相依的情況
說明:以一個透過RSA的token,進行驗證的簡化範例來進行說明。
重點在於:RSA的token,就像每秒會改變的亂數產生器。如果驗證的方法,直接相依於Token類別,則幾乎無法進行測試,因為每次的值都不一樣。
驗證的程式
public class Authentication
{
/// <summary>
/// 透過id去尋找設定的password,透過id尋找token對應的password,兩者相加才是合法的密碼
/// </summary>
/// <param name="id">The id.</param>
/// <param name="inputPassword">The input password.</param>
/// <returns></returns>
/// <history>
/// 1. joeychen, 2011/5/12, Created
/// </history>
public bool Verify(string id, string inputPassword)
{
Token token = new Token();
string tokenPassword = token.Generate(id);
Person person = new Person(id);
string password = person.GetPassword();
string validPassword = password + tokenPassword;
return (inputPassword == validPassword);
}
}
Token的程式(以亂數產生器模擬)
internal class Token
{
internal string Generate(string id)
{
return new Random().Next(0, 999999).ToString("000000");
}
}
Person的程式(以一個回傳hardcode的類別來當作DAO)
public class Person
{
string _id = string.Empty;
public Person(string id)
{
this._id = id;
}
/// <summary>
/// 根據this._id去DB撈Password資料
/// </summary>
/// <returns></returns>
internal string GetPassword()
{
return "91";
}
}
幾乎每次都會測試失敗的單元測試程式
/// <summary>
///Verify 的測試
///</summary>
[TestMethod()]
public void VerifyTest()
{
//Arrange
Authentication target = new Authentication();
//Act
bool result = target.Verify("joey", "123456789");
//Assert
//將驗證失敗,因為每一次的亂數,都不固定,所以Act結果是123456789的機率幾乎為0
Assert.AreEqual(true, result);
}
測試目標類別相依於介面,透過mock framework進行測試
原本的Authetication類別,將相依於Token與Person,改相依於IToken與IPerson
public class AuthenticationInteractWithInterface
{
public IToken MyToken { get; set; }
public IPerson MyPerson { get; set; }
public bool Verify(string id, string inputPassword)
{
//Token token = new Token();
string tokenPassword = MyToken.Generate(id);
//Person person= new Person(id);
string password = MyPerson.GetPassword();
string validPassword = password + tokenPassword;
return (inputPassword == validPassword);
}
}
使用mock framework來進行測試的單元測試程式(這邊以Rhino.Mock為例)
/// <summary>
///Verify 的測試
///</summary>
[TestMethod()]
public void VerifyTest()
{
//Arrange
//當呼叫IPerson.GetPassword()的時候,回傳"123"
IPerson person = MockRepository.GenerateStub<IPerson>();
person.Stub(x => x.GetPassword()).Return("123");
//當呼叫IToken.Generate(),且參數為"joey"時,回傳"456789"
IToken token = MockRepository.GenerateStub<IToken>();
token.Stub(x => x.Generate("joey")).Return("456789");
AuthenticationInteractWithInterface target = new AuthenticationInteractWithInterface { MyPerson = person, MyToken = token };
//Act
//預期結果為true,因為inputPassword為"123456789",而在白箱測試中,我們可以預期Person+Token所組出的password也為"123456789",所以為true
bool result = target.Verify("joey", "123456789");
//Assert
Assert.AreEqual(true, result);
}
結論
測試程式一點都不難寫,因為測試程式通常相當短,而且不具邏輯,就是一段直述型的function。
當您發現您的測試程式相當難寫、很難透過stub/mock來測試、測試程式相當不穩定時,就代表您實際運行的程式碼,設計得不夠好。
底下歸納出幾種狀況,以供參考。
- 測試程式很長:可能代表測試目標類別的內聚力不夠高。
- 很難透過stub/mock來測試:可能代表測試目標類別之間耦合性太高,類別之間直接相依。
- 測試程式很不穩定:可能代表測試目標類別職責太複雜,或是抽象地不夠漂亮,導致某一種職責變更時,測試程式就需要跟著改變。
單元測試應該具有FIRST特性:
- Fast:快速
- Independent:獨立
- Repeatable:可重複
- Self-Validating:可反應驗證結果。單元測試不論成功或失敗,都應該要從測試的reporting直接瞭解其意義或失敗原因
- Timely:及時。
Reference
blog 與課程更新內容,請前往新站位置:http://tdd.best/