TDD中的單元測試寫多少纔夠?

測試驅動開發(TDD)已是耳熟能詳的名詞,既然是測試驅動,那麼測試用例代碼就要寫在開發代碼的前面。可是如何寫測試用例?寫多少測試用例纔夠?我想你們在實際的操做過程都會產生這樣的疑問。編程

3月15日,我參加了thoughtworks組織的「結對編程和TDD Openworkshop」活動,聆聽了tw的資深諮詢專家仝(tong2)鍵的精彩講解,並在講師的帶領下實際參與了一次TDD和結對編程的過程。活動中,仝鍵老師對到底寫多少測試用例纔夠的問題,給出了下面一個解釋:安全

咱們寫單元測試,有一個重要的緣由是用來防止本身犯低級錯誤的。咱們不能把寫實現代碼的人看成咱們的敵人,必定要把所有狀況都測到,以防止他們在裏面故意留下各類隱蔽的陷阱。測試寫的再多可能也沒有辦法覆蓋所有狀況,因此只要能讓本身感到安全便可。怎樣才能讓本身感到安全呢?這是沒有標準答案的,只能是寫多了測試之後慢慢體會。 app

另外,寫測試也要花時間的,好比compare這個方法的實現部分,咱們只花了一兩分鐘就寫完了,而這些測試代碼,咱們花了足足半個多小時,這樣作值得嗎?對於簡單的業務邏輯來講,固然是不值得的,畢竟咱們還不少工做等着作,老闆花錢是爲了咱們的產品代碼,而不是測試代碼。 ide

再考慮一種狀況,我要創業,想了一個點子,作了一個網站,我固然是想以最快的速度把它作成型讓別人用。若是我在徹底不知道人們會不會喜歡的時候,先花大量時間寫測試,最後發現沒人用只能丟掉,這些測試豈不是白寫了。 函數

因此仍是上面那句話:單元測試是讓你提高本身對代碼的信心的,只要你感受安全能夠繼續開發時就夠了,不是越多越好。單元測試

我相信上面一段解釋對於本文中提出的問題你們都沒有什麼異議。可是這裏咱們不考慮特殊狀況,在實際操做中,是否有辦法對單元測試這一工做進行衡量?來判斷是否足夠?測試

 

使用代碼覆蓋率來衡量單元測試是否足夠網站

常見的代碼覆蓋率有下面幾種:spa

  • 語句覆蓋(Statement Coverage):這是最經常使用也是最多見的一種覆蓋方式,就是度量被測代碼中每一個可執行語句是否被執行到了。
  • 斷定覆蓋(Desicion Coverage):它度量程序中每個斷定的分支是否都被測試到了。
  • 條件覆蓋(Condition Coverage):它度量斷定中的每一個子表達式結果true和false是否被測試到了。
  • 路徑覆蓋(Path Coverage):它度量了是否函數的每個分支都被執行了。

前三種覆蓋率你們能夠查看下面的引用的第3篇文章,這裏就再也不多說。咱們經過一個例子,來看看路徑覆蓋。好比下面的測試代碼中有兩個斷定分支.net

int foo(int a, int b)
{
int nReturn = 0;
if (a < 10)
{// 分支一
nReturn+= 1;
}
if (b < 10)
{// 分支二
nReturn+= 10;
}
return nReturn;
}

咱們仔細看看邏輯,nReturn的結果一共有4種可能,咱們經過路徑覆蓋的方法設計出來的測試用例:
用例 參數 返回值
Test Case 1 a=5, b=5 0
Test Case 2 a=15, b=5 1
Test Case 3 a=5, b=15 10
Test Case 1 a=15, b=15 11

Perfect。可是實際中的代碼每每比上面的例子複雜,若是代碼中有5個if-else,那麼按照路徑覆蓋的方法,至少須要25=32個測試用例。這樣簡直要瘋掉了。

 

不必追求代碼覆蓋率,真正要覆蓋的是邏輯

簡單追求代碼結構上的覆蓋率,容易致使產生大量無心義的測試用例或者沒法覆蓋關鍵業務邏輯。咱們再看看上面解釋的第一段話。

咱們寫單元測試,有一個重要的緣由是用來防止本身犯低級錯誤的。咱們不能把寫實現代碼的人看成咱們的敵人,必定要把所有狀況都測到,以防止他們在裏面故意留下各類隱蔽的陷阱。測試寫的再多可能也沒有辦法覆蓋所有狀況,因此只要能讓本身感到安全便可。怎樣才能讓本身感到安全呢?這是沒有標準答案的,只能是寫多了測試之後慢慢體會。

怎麼纔算讓本身感到安全?覆蓋邏輯,而不是代碼。站在使用者的角度考慮,須要關心的是軟件實現邏輯,而不是覆蓋率。以下面的例子:

public class UserBusiness
{
public string CreateUser(User user)
{
string result = "success";

if (string.IsNullOrEmpty(user.Username))
{
result = "usename is null or empty";
}
else if (string.IsNullOrEmpty(user.Password))
{
result = "password is null or empty";
}
else if (user.Password != user.ConfirmPassword)
{
result = "password is not equal to confirmPassword";
}
else if (string.IsNullOrEmpty(user.Creator))
{
result = "creator is null or empty";
}
else if (user.CreateDate == new DateTime())
{
result = "createdate must be assigned value";
}
else if (string.IsNullOrEmpty(user.CreatorIP))
{
result = "creatorIP is null or empty";
}

if (result != "success")
{
return result;
}

user.Username = user.Username.Trim();
user.Password = BitConverter.ToString(MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(user.Password)));

UserDataAccess dataAccess = new UserDataAccess();
dataAccess.CreateUser(user);

return result;
}
}

在寫UserBusiness.CreateUser的測試用例的時候,咱們定義了下面幾個單元測試用例:
[TestClass()]
public class UserBusinessTest
{
private TestContext testContextInstance;

/// <summary>
///Gets or sets the test context which provides
///information about and functionality for the current test run.
///</summary>
public TestContext TestContext
{
get
{
return testContextInstance;
}
set
{
testContextInstance = value;
}
}

[TestMethod()]
public void Should_Username_Not_Null_Or_Empty()
{
UserBusiness target = new UserBusiness();
User user = new User();
string expected = "usename is null or empty";
string actual = target.CreateUser(user);
Assert.AreEqual(expected, actual);
}

[TestMethod()]
public void Should_Password_Not_Null_Or_Empty()
{
UserBusiness target = new UserBusiness();
User user = new User()
{
Username = "ethan.cai"
};
string expected = "password is null or empty";
string actual = target.CreateUser(user);
Assert.AreEqual(expected, actual);
}

[TestMethod()]
public void Should_Password_Equal_To_ConfirmPassword()
{
UserBusiness target = new UserBusiness();
User user = new User()
{
Username = "ethan.cai",
Password = "a121ww123",
ConfirmPassword = "a121ww1231"
};
string expected = "password is not equal to confirmPassword";
string actual = target.CreateUser(user);
Assert.AreEqual(expected, actual);
}

[TestMethod()]
public void Should_Creator_Not_Null_Or_Empty()
{
UserBusiness target = new UserBusiness();
User user = new User()
{
Username = "ethan.cai",
Password = "a121ww123",
ConfirmPassword = "a121ww1231"
};
string expected = "password is not equal to confirmPassword";
string actual = target.CreateUser(user);
Assert.AreEqual(expected, actual);
}

[TestMethod()]
public void Should_CreateDate_Assigned_Value()
{
UserBusiness target = new UserBusiness();
User user = new User()
{
Username = "ethan.cai",
Password = "a121ww123",
ConfirmPassword = "a121ww123",
Creator = "ethan.cai"
};
string expected = "createdate must be assigned value";
string actual = target.CreateUser(user);
Assert.AreEqual(expected, actual);
}

[TestMethod()]
public void Should_CreatorIP_Not_Null_Or_Empty()
{
UserBusiness target = new UserBusiness();
User user = new User()
{
Username = "ethan.cai",
Password = "a121ww123",
ConfirmPassword = "a121ww123",
Creator = "ethan.cai",
CreateDate = DateTime.Now
};
string expected = "creatorIP is null or empty";
string actual = target.CreateUser(user);
Assert.AreEqual(expected, actual);
}

[TestMethod()]
public void Should_Trim_Username()
{
UserBusiness target = new UserBusiness();
User user = new User()
{
Username = "ethan.cai ",
Password = "a121ww123",
ConfirmPassword = "a121ww123",
Creator = "ethan.cai",
CreateDate = DateTime.Now,
CreatorIP = "127.0.0.1"
};
string expected = "ethan.cai";
target.CreateUser(user);
Assert.AreEqual(expected, user.Username);
}

[TestMethod()]
public void Should_Save_MD5_Hash_Password()
{
UserBusiness target = new UserBusiness();
User user = new User()
{
Username = "ethan.cai ",
Password = "a121ww123",
ConfirmPassword = "a121ww123",
Creator = "ethan.cai",
CreateDate = DateTime.Now,
CreatorIP = "127.0.0.1"
};

string actual = target.CreateUser(user);
Assert.IsTrue("success" == actual
&& user.Password == BitConverter.ToString(MD5.Create().ComputeHash(Encoding.UTF8.GetBytes("a121ww123"))));
}

[TestMethod()]
public void Should_Create_User_Successfully_When_User_Is_OK()
{
UserBusiness target = new UserBusiness();
User user = new User()
{
Username = "ethan.cai ",
Password = "a121ww123",
ConfirmPassword = "a121ww123",
Creator = "ethan.cai",
CreateDate = DateTime.Now,
CreatorIP = "127.0.0.1"
};
string expected = "success";
string actual = target.CreateUser(user);
Assert.IsTrue(expected == actual);
}
}
 
image

若是僅從代碼覆蓋率的角度來看,單元測試Should_Trim_Username、Should_Save_MD5_Hash_Password不會增長覆蓋率,彷佛沒有必要,可是從邏輯上看,建立的帳戶的Username頭尾不能包含空白字符,密碼也不能明文存儲,顯然這兩個用例是很是有必要的。
 
單元測試寫多少纔夠?這個問題沒有肯定的答案,但原則是讓你本身以爲安全。代碼覆蓋率高不能保證安全,真正的安全須要用測試用例覆蓋邏輯。
 

參考文章:

相關文章
相關標籤/搜索