一门技术或一个解决方案的诞生的诞生,不可能凭空去创造,往往是问题而催生出来的。我在做研发负责人的时候饱受深夜加班上线之苦,其中提到的两个大问题一个是部署问题,另一个就是测试问题。部署问题,我们引入了自动化的部署。我们要做持续集成,剩下的就是测试问题了。
回归测试成了我们的第一大问题。随着我们项目的规模与复杂度的提升,我们的回归测试变得越来越困难。由于我们的当时的测试全依赖手工测试,我们项目的迭代周期大概在一个月左右,而测试的时间就要花费一半多的时间。甚至版本上线以后做一遍回归测试就需要几个小时的时间。而且这种手工进行的功能性测试很容易有遗漏的地方,因此线上Bug层出不穷。一堆问题困扰着我们,我们不得不考虑进行自动化的测试。
自动化测试同样不是银弹,自动化测试虽然与手工测试相比有其优点,其测试效率高,资源利用率高(一般白天开发写用例,晚上自动化程序跑),可以进行压力、负载、并发、重复等人力不可完成的测试任务,执行效率较快,执行可靠性较高,测试脚本可重复利用,bug及时发现.......但也有其不可避免的缺点,如:只适合回归测试,开发中的功能或者变更频繁的功能,由于变更频繁而不断更改测试脚本是不划算的,并且脚本的开发也需要高水平的测试人员和时间......总体来说,虽然自动化的测试可以解决一部分的问题,但也同样会带来另一些问题。到底应该不应该引入自动化的测试还需要结合自己公司的团队现状来综合考虑。
什么是单元测试
我们先来看几个常见的对单元测试的定义。用最简单的话说:单元测试就是针对一个工作单元设计的测试,这里的“工作单元”是指对一个工作方法的要求。
单元测试是开发者编写的一小段代码,用于检测被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试用于判断某个特定条件(或场景)下某个特定函数的行为。
例:你可能把一个很大的值放入一个有序list中去,然后确认该值出现在list的尾部。或者,你可能会从字符串中删除匹配某种模式的字符,然后确认字符串确实不再包含这些字符了。
执行单元测试,就是为了证明某段代码的行为和开发者所期望的一致!
什么不是单元测试
这里我们暂且先将其分为三种情况:
1.跨边界的测试
单元测试背后的思想是,仅测试这个方法中的内容,测试失败时不希望必须穿过基层代码、数据库表或者第三方产品的文档去寻找可能的答案!
当测试开始渗透到其他类、服务或系统时,此时测试便跨越了边界,失败时会很难找到缺陷的代码。
测试跨边界时还会产生另一个问题,当边界是一个共享资源时,如数据库。与团队的其他开发人员共享资源时,可能会污染他们的测试结果!
2.不具有针对性的测试
如果发现所编写的测试对一件以上的事情进行了测试,就可能违反了“单一职责原则”。从单元测试的角度来看,这意味着这些测试是难以理解的非针对性测试。随着时间的推移,向类或方法种添加了更多的不恰当的功能后,这些测试可能会变的非常脆弱。诊断问题也将变得极具有挑战性。
如:StringUtility中计算一个特定字符在字符串中出现的次数,它没有说明这个字符在字符串中处于什么位置也没有说明除了这个字符出现多少次之外的其他任何信息,那么这些功能就应该由StringUtility类的其它方法提供!同样,StringUtility类也不应该处理数字、日期或复杂数据类型的功能!
3.不可预测的测试
单元测试应当是可预测的。在针对一组给定的输入参数调用一个类的方法时,其结果应当总是一致的。有时,这一原则可能看起来很难遵守。例如:正在编写一个日用品交易程序,黄金的价格可能上午九时是一个值,14时就会变成另一个值。
如何去做单元测试
简单示例(基于Nunit)
/// <summary> /// 计算器类 /// </summary> public class Calculator { /// <summary> /// 加法 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> public double Add(double a, double b) { return a + b; } /// <summary> /// 减法 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> public double Sub(double a, double b) { return a - b; } /// <summary> /// 乘法 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> public double Mutiply(double a, double b) { return a * b; } /// <summary> /// 除法 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> public double Divide(double a, double b) { return a / b; } } |
/// <summary> /// 针对计算加减乘除的简单的单元测试类 /// </summary> [TestFixture] public class CalculatorTest { /// <summary> /// 计算器类对象 /// </summary> public Calculator Calculator { get; set; } /// <summary> /// 参数1 /// </summary> public double NumA { get; set; } /// <summary> /// 参数2 /// </summary> public double NumB { get; set; } /// <summary> /// 初始化 /// </summary> [SetUp] public void SetUp() { NumA = 10; NumB = 20; Calculator = new Calculator(); } /// <summary> /// 测试加法 /// </summary> [Test] public void TestAdd() { double result = Calculator.Add(NumA, NumB); Assert.AreEqual(result, 30); } /// <summary> /// 测试减法 /// </summary> [Test] public void TestSub() { double result = Calculator.Sub(NumA, NumB); Assert.LessOrEqual(result, 0); } /// <summary> /// 测试乘法 /// </summary> [Test] public void TestMutiply() { double result = Calculator.Mutiply(NumA, NumB); Assert.GreaterOrEqual(result, 200); } /// <summary> /// 测试除法 /// </summary> [Test] public void TestDivide() { double result = Calculator.Divide(NumA, NumB); Assert.IsTrue(0.5 == result); } } |
单元测试是非常有魔力的魔法,但是如果使用不恰当亦会浪费大量的时间在维护和调试上从而影响代码和整个项目。
好的单元测试应该具有以下品质:
· 自动化
· 彻底的
· 可重复的
· 独立的
· 专业的
单元测试之代码覆盖率
在做单元测试时,代码覆盖率常常被拿来作为衡量测试好坏的指标,甚至,用代码覆盖率来考核测试任务完成情况,比如,代码覆盖率必须达到80%或90%。于是乎,测试人员费尽心思设计案例覆盖代码。因此我认为用代码覆盖率来衡量是不合适的,我们最根本的目的是为了提高我们回归测试的效率,项目的质量不是吗?
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理