测试驱动开发(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);
}
}
参考文章: