单元测试基础
1.1单元测试的定义#
单元测试就是针对一个工作单元设计的测试,这里的“工作单元”是指对一个工作方法的要求。
单元测试是开发者编写的一小段代码,用于检测被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试用于判断某个特定条件(或场景)下某个特定函数的行为。
例:
你可能把一个很大的值放入一个有序list中去,然后确认该值出现在list的尾部。或者,你可能会从字符串中删除匹配某种模式的字符,然后确认字符串确实不再包含这些字符了。
执行单元测试,就是为了证明某段代码的行为和开发者所期望的一致!
//被测方法
public double Add(double a, double b)
{
return a + b;
}
//测试方法
[Test]
public void AddTest()
{
double result = new Calculator().Add(14, 15);
Assert.AreEqual(30,result);
}
1.2工作单元#
调用系统的一个公共方法到产生一个测试可见的最终结果,其间这个系统发生的行为总称为一个工作单元。我们通过系统的公共AP和行为就可以观察到一个可见的最终结果,无需查看系统的内部状态。一个最终结果可以是以下任何一种形式。
- 被调用的公共方法回一个值(一个返回值不为空的函数)
- 在方法调用的前后,系统的状态或行为有可见的变化,这种变化无需查询私有状态即可判断。(例如:一个以前不存在的用户可以登入系统,或者一个状态机系统的属性发生变化。)
- 调用了一个不受测试控制的第三方系统,这个第三方系统不返回任何值,或者返回值都被忽略。(例如:调用一个第三方日志系统,这个系统不是你编写的,而且你也没有源代码。)
很多人觉得被测试的工作单元应该尽可能的小。我却不这么看,我认为工作单元这个概念意味着一个单元既可以小到只包含一个方法,也可以大到包括实现某个功能的多个类和函数。如果你的工作单元很大,却但是其最终结果对用户可见度高,易于维护也未尝不是好的测试,相反如果试图把工作单元缩到最小,最后会不得不伪造一堆东西反而会增加测试的复杂度,适得其反。
2.什么不是单元测试#
单元测试其实是一门很基础也很简单的技术,然而在单元测试实践过程中,往往会对单元测试产生一些误区,进而写出一些不是单元测试的"单元测试" ,其中常见的主要有以下三种。
2.1 跨边界的测试#
单元测试背后的思想是,仅测试这个方法中的内容,测试失败时不希望必须穿过基层代码、数据库表或者第三方产品的文档去寻找可能的答案!
当测试开始渗透到其他类、服务或系统时,此时测试便跨越了边界,失败时会很难找到缺陷的代码。
测试跨边界时还会产生另一个问题,当边界是一个共享资源时,如数据库。与团队的其他开发人员共享资源时,可能会污染他们的测试结果!
2.2 不具有针对性的测试#
如果发现所编写的测试对一件以上的事情进行了测试,就可能违反了“单一职责原则”。从单元测试的角度来看,这意味着这些测试是难以理解的非针对性测试。随着时间的推移,向类或方法种添加了更多的不恰当的功能后,这些测试可能会变的非常脆弱。诊断问题也将变得极具有挑战性。
如:StringUtility中计算一个特定字符在字符串中出现的次数,它没有说明这个字符在字符串中处于什么位置也没有说明除了这个字符出现多少次之外的其他任何信息,那么这些功能就应该由StringUtility类的其它方法提供!同样,StringUtility类也不应该处理数字、日期或复杂数据类型的功能!
2.3 不可预测的测试#
单元测试应当是可预测的。在针对一组给定的输入参数调用一个类的方法时,其结果应当总是一致的。有时,这一原则可能看起来很难遵守。例如:正在编写一个日用品交易程序,黄金的价格可能上午九时是一个值,14时就会变成另一个值。
而好的设计原则就是将不可预测的数据的功能抽象到一个可以在单元测试中模拟(Mock)的类或方法中
2.4 集成测试#
其实上面三种测试已经到了集成测试的领域。任何测试,如果它运行速度不快,结果不稳定,或者要用到被测试单元的一个或多个真实依赖物,我们就认为它是集成测试。
集成测试是对一个工作单元进行的测试,这个测试对被测试的工作单元没有完全的控制,并使用该单元的一个或多个真实依赖物,例如时间、网络、数据库、线程或随机数产生器等。
集成测试本身并不是一种坏事,反而其具有和单元测试一样高的地位,但是在实践过程中我们把集成测试和单元测试分离开来还是很重要的。
3.优秀的单元测试有哪些特性#
单元测试是非常有魔力的魔法,也是一把双刃剑。使用得当,可以很有效的提高我们的编码质量,提升研发效率,但是如果使用不恰当亦会浪费大量的时间在测试编码、维护和调试上从而影响代码和整个项目,徒劳而无功!
因此做好单元测试至关重要!而想要做好单元测试,我们首先应该知道优秀的单元测试有哪些特性。
一个好的单元测试一定是有以下几个特性的
• 自动化
• 彻底的
• 可重复的
• 独立的
• 专业的
回顾一下自己以前写过的单元测试问自己几个问题。
-
它是不是可以自动化一键运行、并且可以重复运行
-
几个月后它是不是仍可以运行、并且得到期望的结果
-
它是否可以在几分钟内运行结束
-
在运行之前你是否不需要需要进行一系列的配置
-
每次运行是否能够得到相同的结果
-
外部的系统因素是否不会影响你的测试结果
-
测试代码是否很简单就可以编写完成
如果针对以上问题有任何一个的回答是“否”,那么你应该好好的思考一下到底如何去做好单元测试。
4. 如何进行单元测试#
对于一个方法或者类,乍一看就能找出其隐藏深处的bug是很不容易的,因此在bug挖掘方面通常会有一些经验和套路,来指导我们更好的进行单元测试。
3.1 测试哪些内容#
一般来说有六个值得测试的具体方面,可以把这六个方面统称为Right-BICEP:
- Right——结果
对于单元测试测试而言,首要的也是最明显的任务就是查看所期望的结果是否正确,例如判断一个方法的返回值是否为序列中的最大值...... - B——边界条件
找边界条件是做单元测试中最有价值的工作之一,因为bug一般就出现在边界上。关于边界条件2会有详细总结 - I——检查反向关联
对于一些方法,我们可以使用反向的逻辑关系来验证它们。例如,你可以用对结果进行平方的方式来检查一个计算平方根的函数,然后测试结果是否和原数据很接近 - C——交叉检查
有些时候我们实现一个问题会有不同的算法,在生产系统中我们使用一种算法,而在测试中我们可以使用另一种算法来验证其结果是否一致。 - E——强制产生错误条件
在实际运行过程中,有时候会发生一些意外的难以避免的错误,例如磁盘会满,网络连线会断开.....从而导致程序崩溃。我们应该在测试中强制引发错误,来测试代码是否能够按照预期处理这些异常。 - P——是否满足性能条件
性能同样是我们测试过程中需要验证的指标
3.2 注意边界条件#
代码中的许多Bug经常出现在边界条件附近,对于边界条件的测试我们可以从CORRECT七个方面进行考虑
- 一致性----值是否满足预期的格式
- 有序性----一组值是否满足预期的排序要求
- 区间性----值是否在一个合理的最大值最小值范围内
- 引用、耦合性----代码是否引用了一些不受代码本身直接控制的外部因素
- 存在性----值是否存在(例如:非Null,非零,存在于某个集合中)
- 基数性----是否恰好具有足够的值
- 时间性----所有事情是否都按照顺序发生的?是否在正确的时间、是否及时
3.3 使用Mock对象#
单元测试的目标是验证我们的工作单元,但是如果这个工作单元依赖一些其他的对象或是一些难以操控的东西,比如网络、数据库等。这时我们就要使用mock对象,使得在运行UT的时候使用的那些难以操控的东西实际上是我们mock的对象,而我们mock的对象则可以按照我们的意愿返回一些值用于测试。通俗来讲,Mock对象就是真实对象在我们调试期间的测试品。对于外部对象内的逻辑我们并不关心,我们只需要让它给我们返回我们想要的值,来验证我们的业务逻辑即可
IFileExtensionManager fileManager;
public bool IsValidFileName(){
//获取文件扩展名
string extName=fileManager.GetExtName();
if(extName=="jpg"){
return true;
}
return false;
}
如上示例,假设从文件系统中读取一个文件,获取文件的扩展名,如果扩展名是jpg就返回true,否则返回false。
注意,这里我们要测试的逻辑是如果扩展名是jpg就返回true,否则返回false。而对于fileManager.GetExtName()方法内部的逻辑是什么样的的我们是不关心的,我们只需要mock这个方法使其返回我们想要的值就可以了。
关于具体如何去mock工作单元中的一些外部依赖,会在存根与模拟对象里面详细进行总结。
总结#
本文总结了什么是单元测试、什么不是单元测试以及优秀的单元测试有哪些特性,简单介绍了如何进行单元测试。
编写差劲的单元测试是没有意义的,我看到过很多公司尝试去实践单元测试,但最终要么在某个阶段放弃了,要么并没有真正执行单元测试。最终还是依赖集成测试或者人工测试来发现问题,不得不以失败而告终,并堂而皇之的认为单元测试是一个耗时好力而无功的鸡肋东西。
因此如果你想要真正的去实践单元测试,那么必须充分的理解到底什么是单元测试,已经如何去更好的进行实践优秀的单元测试。
而对于如何更好的去实践单元测试,后续会结合实践用更多的篇幅去总结分享。
作者: 老于`