測試驅動開發(TDD)已是耳熟能詳的名詞,既然是測試驅動,那麼測試用例代碼就要寫在開發代碼的前面。可是如何寫測試用例?寫多少測試用例纔夠?我想你們在實際的操做過程都會產生這樣的疑問。編程
3月15日,我參加了thoughtworks組織的「結對編程和TDD Openworkshop」活動,聆聽了tw的資深諮詢專家仝(tong2)鍵的精彩講解,並在講師的帶領下實際參與了一次TDD和結對編程的過程。活動中,仝鍵老師對到底寫多少測試用例纔夠的問題,給出了下面一個解釋:安全
咱們寫單元測試,有一個重要的緣由是用來防止本身犯低級錯誤的。咱們不能把寫實現代碼的人看成咱們的敵人,必定要把所有狀況都測到,以防止他們在裏面故意留下各類隱蔽的陷阱。測試寫的再多可能也沒有辦法覆蓋所有狀況,因此只要能讓本身感到安全便可。怎樣才能讓本身感到安全呢?這是沒有標準答案的,只能是寫多了測試之後慢慢體會。 app
另外,寫測試也要花時間的,好比
compare
這個方法的實現部分,咱們只花了一兩分鐘就寫完了,而這些測試代碼,咱們花了足足半個多小時,這樣作值得嗎?對於簡單的業務邏輯來講,固然是不值得的,畢竟咱們還不少工做等着作,老闆花錢是爲了咱們的產品代碼,而不是測試代碼。 ide再考慮一種狀況,我要創業,想了一個點子,作了一個網站,我固然是想以最快的速度把它作成型讓別人用。若是我在徹底不知道人們會不會喜歡的時候,先花大量時間寫測試,最後發現沒人用只能丟掉,這些測試豈不是白寫了。 函數
因此仍是上面那句話:單元測試是讓你提高本身對代碼的信心的,只要你感受安全能夠繼續開發時就夠了,不是越多越好。單元測試
我相信上面一段解釋對於本文中提出的問題你們都沒有什麼異議。可是這裏咱們不考慮特殊狀況,在實際操做中,是否有辦法對單元測試這一工做進行衡量?來判斷是否足夠?測試
使用代碼覆蓋率來衡量單元測試是否足夠網站
常見的代碼覆蓋率有下面幾種:spa
前三種覆蓋率你們能夠查看下面的引用的第3篇文章,這裏就再也不多說。咱們經過一個例子,來看看路徑覆蓋。好比下面的測試代碼中有兩個斷定分支.net
int foo(int a, int b)
{
int nReturn = 0;
if (a < 10)
{// 分支一
nReturn+= 1;
}
if (b < 10)
{// 分支二
nReturn+= 10;
}
return nReturn;
}
用例 | 參數 | 返回值 |
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;
}
}
[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);
}
}
參考文章: