TDD中的单元测试写多少才够?

 

测试驱动开发(TDD)已经是耳熟能详的名词,既然是测试驱动,那么测试用例代码就要写在开发代码的前面。但是如何写测试用例?写多少测试用例才够?我想大家在实际的操作过程都会产生这样的疑问。

3月15日,我参加了thoughtworks组织的“结对编程和TDD Openworkshop”活动,聆听了tw的资深咨询专家仝(tong2)键的精彩讲解,并在讲师的带领下实际参与了一次TDD和结对编程的过程。活动中,仝键老师对到底写多少测试用例才够的问题,给出了下面一个解释:

我们写单元测试,有一个重要的原因是用来防止自己犯低级错误的。我们不能把写实现代码的人当作我们的敌人,一定要把全部情况都测到,以防止他们在里面故意留下各种隐蔽的陷阱。测试写的再多可能也没有办法覆盖全部情况,所以只要能让自己感到安全即可。怎样才能让自己感到安全呢?这是没有标准答案的,只能是写多了测试以后慢慢体会。

另外,写测试也要花时间的,比如compare这个方法的实现部分,我们只花了一两分钟就写完了,而这些测试代码,我们花了足足半个多小时,这样做值得吗?对于简单的业务逻辑来说,当然是不值得的,毕竟我们还很多工作等着做,老板花钱是为了我们的产品代码,而不是测试代码。

再考虑一种情况,我要创业,想了一个点子,做了一个网站,我当然是想以最快的速度把它做成型让别人用。如果我在完全不知道人们会不会喜欢的时候,先花大量时间写测试,最后发现没人用只能丢掉,这些测试岂不是白写了。

所以还是上面那句话:单元测试是让你提升自己对代码的信心的,只要你感觉安全可以继续开发时就够了,不是越多越好。

我相信上面一段解释对于本文中提出的问题大家都没有什么异议。但是这里我们不考虑特殊情况,在实际操作中,是否有办法对单元测试这一工作进行衡量?来判断是否足够?

使用代码覆盖率来衡量单元测试是否足够

常见的代码覆盖率有下面几种:

  • 语句覆盖(Statement Coverage):这是最常用也是最常见的一种覆盖方式,就是度量被测代码中每个可执行语句是否被执行到了。
  • 判定覆盖(Desicion Coverage):它度量程序中每一个判定的分支是否都被测试到了。
  • 条件覆盖(Condition Coverage):它度量判定中的每个子表达式结果true和false是否被测试到了。
  • 路径覆盖(Path Coverage):它度量了是否函数的每一个分支都被执行了。

前三种覆盖率大家可以查看下面的引用的第3篇文章,这里就不再多说。我们通过一个例子,来看看路径覆盖。比如下面的测试代码中有两个判定分支

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);
}
}
 
如果仅从代码覆盖率的角度来看,单元测试Should_Trim_Username、Should_Save_MD5_Hash_Password不会增加覆盖率,似乎没有必要,但是从逻辑上看,创建的账户的Username头尾不能包含空白字符,密码也不能明文存储,显然这两个用例是非常有必要的。
 
单元测试写多少才够?这个问题没有确定的答案,但原则是让你自己觉得安全。代码覆盖率高不能保证安全,真正的安全需要用测试用例覆盖逻辑。
 

参考文章:

TDD中的单元测试的更多相关文章

  1. TDD中的单元测试写多少才够?

    测试驱动开发(TDD)已经是耳熟能详的名词,既然是测试驱动,那么测试用例代码就要写在开发代码的前面.但是如何写测试用例?写多少测试用例才够?我想大家在实际的操作过程都会产生这样的疑问. 3月15日,我 ...

  2. 真正意义上的spring环境中的单元测试方案spring-test与mokito完美结合

    真正意义上的spring环境中的单元测试方案spring-test与mokito完美结合 博客分类: java 测试 单元测试SpringCC++C#  一.要解决的问题:     spring环境中 ...

  3. Visual Studio 中的单元测试 UNIT TEST

    原文:Visual Studio 中的单元测试 UNIT TEST 注:本文系作者原创,可随意转载,但请注明出处.如实在不愿注明可留空,强烈反对更改原创出处.TDD(Test-Driven Devel ...

  4. 我的TDD实践---UnitTest单元测试

    我的TDD实践---UnitTest单元测试 “我的TDD实践”系列之UnitTest单元测试 写在前面: 我的TDD实践这几篇文章主要是围绕测试驱动开发所展开的,其中涵盖了一小部分测试理论,更多的则 ...

  5. 在Nodejs中贯彻单元测试

    在团队合作中,你写好了一个函数,供队友使用,跑去跟你的队友说,你传个A值进去,他就会返回B结果了.过了一会,你队友跑过来说,我传个A值却返回C结果,怎么回事?你丫的有没有测试过啊? 大家一起写个项目, ...

  6. Visual Studio中UnitTesting单元测试模板代码生成

             在软件研发过程中,单元测试的重要性直接影响软件质量.经验表明一个尽责的单元测试方法将会在软件开发的某个阶段发现很多的Bug,并且修改它们的成本也很低.在软件开发的后期阶段,Bug的发 ...

  7. 在Android中进行单元测试遇到的问题

    问题1.Cannot connect to VM  socket closed 在使用JUnit进行测试的时候,遇到这个问题.网上的解释是:使用Eclipse对Java代码进行调试,无论是远程JVM还 ...

  8. TDD中的迭代

    博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:TDD中的迭代.

  9. 在Android Studio中进行单元测试和UI测试

    本篇教程翻译自Google I/O 2015中关于测试的codelab,掌握科学上网的同学请点击这里阅读:Unit and UI Testing in Android Studio.能力有限,如有翻译 ...

随机推荐

  1. ZOJ-3652-Maze(BFS)

    Celica is a brave person and believer of a God in the bright side. He always fights against the mons ...

  2. 网络资源(3) - iBatis视频

    2018_08_24 http://v.youku.com/v_show/id_XMjk2ODY2OTE2.html iBatis视频教程01

  3. MD5算法的原理与实现

    ***********************************************声明************************************************ 原创 ...

  4. NYOJ 904 search(stable_sort函数的应用)

    描述 游戏积分的排行榜出来了,小z想看看得某个积分的人是谁.但是由于人数很多,他自己找很浪费时间,所以他想请你帮忙写一个程序,能快速的帮他找到他想要找的人   输入 多组测试数据,第一行有一个数T,表 ...

  5. 汉字转拼音 oracle方式 [转]

    oracle汉字转拼音(获得全拼/拼音首字母/拼音截取等)   效果如下: Oracle 字符集 GBK 没有问题 , UTF -8 需要修改一下   Sql代码   --oracle汉字转拼音 PA ...

  6. R语言数据分析系列六

    R语言数据分析系列六 -- by comaple.zhang 上一节讲了R语言作图,本节来讲讲当你拿到一个数据集的时候怎样下手分析,数据分析的第一步.探索性数据分析. 统计量,即统计学里面关注的数据集 ...

  7. MySql创建一个存储过程

    MySQL 存储过程是从 MySQL 5.0 新功能.存储过程的长处有一箩筐.只是最基本的还是运行效率和SQL 代码封装. 特别是 SQL 代码封装功能,假设没有存储过程,在外部程序訪问数据库时(比如 ...

  8. 转载Worktile 技术架构概要

    Worktile 技术架构概要 其实早就该写这篇博客了,一直说忙于工作没有时间,其实时间挤挤总会有的,可能就是因为懒吧!从2013年11月一直拖到现在,今天就简单谈谈 Worktile 的技术架构吧 ...

  9. [ 夜间模式 ] NightVersion

    DKNightVersion框架.重写管理类 & 控件的分类!--可重写{ 使用GCD.runtime.delegate等 & 工具类的创建 } ================ 1. ...

  10. Java数据结构与算法(2) - ch03排序(冒泡、插入和选择排序)

    排序需要掌握的有冒泡排序,插入排序和选择排序.时间为O(N*N). 冒泡排序: 外层循环从后往前,内存循环从前往后到外层循环,相邻数组项两两比较,将较大的值后移. 插入排序: 从排序过程的中间开始(程 ...