持续集成之单元测试篇——WWH(讲讲咱们作单元测试的故事)

文章导航-readme

持续集成之单元测试篇——WWH(讲讲咱们作单元测试的故事)

前言

  • 临近上线的几天内非重大bug不敢进行发版修复,担忧引发其它问题(摁下葫芦浮起瓢)
  • 尽管咱们如此当心,仍不能避免修改一些bug而引发更多的bug的现象
  • 每每有些bug已经测试经过了可是又复现了
  • 咱们明明没有改动过的功能,却出了问题
  • 有些很明显的bug每每在测试后期甚至到了线上才发现,而此时修复的代价极其之大。
  • 测试时间与周期太长而且质量得不到保障
  • 项目与服务愈来愈多,测试人员严重不足(后来甚至一个研发两个测试人员比)
  • 上线的时候仅仅一轮回归测试就须要几个小时甚至更久
  • 无休止的加班上线。。。

若是你对以上问题很是熟悉,那么我想你的团队和咱们遇到了相同的问题。html

WWH:Why,What,How为何要作单元测试,什么事单元测试,如何作单元测试。数据库

1、为何咱们要作单元测试

1.1 问题滋生解决方案——自动化测试

    一门技术或一个解决方案的诞生的诞生,不可能凭空去创造,每每是问题而催生出来的。在个人.NET持续集成与自动化部署之路第一篇(半天搭建你的Jenkins持续集成与自动化部署系统)这篇文章中提到,我在作研发负责人的时候饱受深夜加班上线之苦,其中提到的两个大问题一个是部署问题,另外一个就是测试问题。部署问题,咱们引入了自动化的部署(后来咱们作到了几分钟就能够上线)。咱们要作持续集成,剩下的就是测试问题了。编程

持续集成

    回归测试成了咱们的第一大问题。随着咱们项目的规模与复杂度的提高,咱们的回归测试变得愈来愈困难。因为咱们的当时的测试全依赖手工测试,咱们项目的迭代周期大概在一个月左右,而测试的时间就要花费一半多的时间。甚至版本上线之后作一遍回归测试就须要几个小时的时间。并且这种手工进行的功能性测试很容易有遗漏的地方,所以线上Bug层出不穷。一堆问题困扰着咱们,咱们不得不考虑进行自动化的测试。json

    自动化测试一样不是银弹,自动化测试虽然与手工测试相比有其优势,其测试效率高,资源利用率高(通常白天开发写用例,晚上自动化程序跑),能够进行压力、负载、并发、重复等人力不可完成的测试任务,执行效率较快,执行可靠性较高,测试脚本可重复利用,bug及时发现.......但也有其不可避免的缺点,如:只适合回归测试,开发中的功能或者变动频繁的功能,因为变动频繁而不断更改测试脚本是不划算的,而且脚本的开发也须要高水平的测试人员和时间......整体来讲,虽然自动化的测试能够解决一部分的问题,但也一样会带来另外一些问题。到底应该不该该引入自动化的测试还须要结合本身公司的团队现状来综合考虑。c#

    而咱们的团队从短时间来看引入自动化的测试其必然会带来一些问题,但长远来看其优势仍是要大于其缺陷的,所以咱们决定作自动化的测试,固然这具体是否是另外一个火坑还须要时间来断定!浏览器

1.2 认识自动化测试金字塔

测试金字塔

    以上即是经典的自动化测试金字塔。网络

    位于金字塔顶端的是探索性测试,探索性测试并无具体的测试方法,一般是团队成员基于对系统的理解,以及基于现有测试没法覆盖的部分,作出系统性的验证,譬如:跨浏览器的测试,一些视觉效果的测试等。探索性测试因为这类功能变动比较频繁,并且所有实现自动化成本较高,所以小范围的自动化的测试仍是有效的。并且其强调测试人员的主观能动性,也不太容易经过自动化的测试来实现,更多的是手工来完成。所以其成本最高,难度最大,反馈周期也是最慢的。并发

    而在测试金字塔的底部是单元测试,单元测试是针对程序单元的检测,一般单元测试都能经过自动化的方式运行,单元测试的实现成本较低,运行效率较高,可以经过工具或脚本彻底自动化的运行,此外,单元测试的反馈周期也是最快的,当单元测试失败后,可以很快发现,而且可以较容易的找到出错的地方并修正。重要的事单元测试通常由开发人员编写完成。(这一点很重要,由于在我这个二线小城市里,可以编写代码的测试人员实在是罕见!)框架

    在金字塔的中间部分,自底向上还包括接口(契约)测试,集成测试,组件测试以及端到端测试等,这些测试侧重点不一样,所使用的技术方法工具等也不相同。ide

    整体而言,在测试金字塔中,从底部到顶部业务价值的比重逐渐增长,即越顶部的测试其业务价值越大,但其成本也愈来愈大,而越底部的测试其业务价值虽小,但其成本较低,反馈周期较短,效率也更高。

1.3 从单元测试开始

    咱们要开始作自动化测试,但不可能一会儿全都作(考虑咱们的人力与技能也作不到)。所以必须有侧重点,考虑良久最终咱们决定从单元测试开始。因而我在刚吃了自动化部署的螃蟹以后,不得不来吃自动化测试的第一个螃蟹。既然决定要作,那么咱们就要先明白单元测试是什么?

2、单元测试是什么

2.1 什么是单元测试。

    咱们先来看几个常见的对单元测试的定义。

    用最简单的话说:单元测试就是针对一个工做单元设计的测试,这里的“工做单元”是指对一个工做方法的要求。

    单元测试是开发者编写的一小段代码,用于检测被测代码的一个很小的、很明确的功能是否正确。一般而言,一个单元测试用于判断某个特定条件(或场景)下某个特定函数的行为。

例:

    你可能把一个很大的值放入一个有序list中去,而后确认该值出如今list的尾部。或者,你可能会从字符串中删除匹配某种模式的字符,而后确认字符串确实再也不包含这些字符了。

执行单元测试,就是为了证实某段代码的行为和开发者所指望的一致!

2.2 什么不是单元测试

    这里咱们暂且先将其分为三种状况

2.2.1 跨边界的测试

    单元测试背后的思想是,仅测试这个方法中的内容,测试失败时不但愿必须穿过基层代码、数据库表或者第三方产品的文档去寻找可能的答案!

    当测试开始渗透到其余类、服务或系统时,此时测试便跨越了边界,失败时会很难找到缺陷的代码。

    测试跨边界时还会产生另外一个问题,当边界是一个共享资源时,如数据库。与团队的其余开发人员共享资源时,可能会污染他们的测试结果!

2.2.2 不具备针对性的测试

    若是发现所编写的测试对一件以上的事情进行了测试,就可能违反了“单一职责原则”。从单元测试的角度来看,这意味着这些测试是难以理解的非针对性测试。随着时间的推移,向类或方法种添加了更多的不恰当的功能后,这些测试可能会变的很是脆弱。诊断问题也将变得极具备挑战性。

    如:StringUtility中计算一个特定字符在字符串中出现的次数,它没有说明这个字符在字符串中处于什么位置也没有说明除了这个字符出现多少次以外的其余任何信息,那么这些功能就应该由StringUtility类的其它方法提供!一样,StringUtility类也不该该处理数字、日期或复杂数据类型的功能!

2.2.3 不可预测的测试

    单元测试应当是可预测的。在针对一组给定的输入参数调用一个类的方法时,其结果应当老是一致的。有时,这一原则可能看起来很难遵照。例如:正在编写一个日用品交易程序,黄金的价格可能上午九时是一个值,14时就会变成另外一个值。

    好的设计原则就是将不可预测的数据的功能抽象到一个能够在单元测试中模拟(Mock)的类或方法中(关于Mock请往下看)。

3、如何去作单元测试

3.1 单元测试框架

    在单元测试框架出现以前,开发人员在建立可执行测试时饱受折磨。最初的作法是在应用程序中建立一个窗口,配有"测试控制工具(harness)"。它只是一个窗口,每一个测试对应一个按钮。这些测试的结果要么是一个消息框,要么是直接在窗体自己给出某种显示结果。因为每一个测试都须要一个按钮,因此这些窗口很快就会变得拥挤、不可管理。

因为人们编写的大多数单元测试都有很是简单的模式:

  • 执行一些简单的操做以创建测试。

  • 执行测试。

  • 验证结果。

  • 必要时重设环境。

因而,单元测试框架应运而生(实际上就像咱们的代码优化中提取公共方法造成组件)。

    单元测试框架(如NUnit)但愿可以提供这些功能。单元测试框架提供了一种统一的编程模型,能够将测试定义为一些简单的类,这些类中的方法能够调用但愿测试的应用程序代码。开发人员不须要编写本身的测试控制工具;单元测试框架提供了测试运行程序(runner),只须要单击按钮就能够执行全部测试。利用单元测试框架,能够很轻松地插入、设置和分解有关测试的功能。测试失败时,测试运行程序能够提供有关失败的信息,包含任何可供利用的异常信息和堆栈跟踪。

​ .Net平台经常使用的单元测试框架有:MSTesting、Nunit、Xunit等。

3.2 简单示例(基于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);
        }
    }

3.3 如何作好单元测试

​ 单元测试是很是有魔力的魔法,可是若是使用不恰当亦会浪费大量的时间在维护和调试上从而影响代码和整个项目。

好的单元测试应该具备如下品质:

• 自动化

• 完全的

• 可重复的

• 独立的

• 专业的

3.3.1 测试哪些内容

​ 通常来讲有六个值得测试的具体方面,能够把这六个方面统称为Right-BICEP:

  • Right----结果是否正确?
  • B----是否全部的边界条件都是正确的?
  • I----可否检查一下反向关联?C----可否用其它手段检查一下反向关联?
  • E----是否能够强制产生错误条件?
  • P----是否知足性能条件?
3.3.2 CORRECT边界条件

​ 代码中的许多Bug常常出如今边界条件附近,咱们对于边界条件的测试该如何考虑?

  • 一致性----值是否知足预期的格式
  • 有序性----一组值是否知足预期的排序要求
  • 区间性----值是否在一个合理的最大值最小值范围内
  • 引用、耦合性----代码是否引用了一些不受代码自己直接控制的外部因素
  • 存在性----值是否存在(例如:非Null,非零,存在于某个集合中)
  • 基数性----是否刚好具备足够的值
  • 时间性----全部事情是否都按照顺序发生的?是否在正确的时间、是否及时
3.3.3 使用Mock对象

    单元测试的目标是一次只验证一个方法或一个类,可是若是这个方法依赖一些其余难以操控的东西,好比网络、数据库等。这时咱们就要使用mock对象,使得在运行unit test的时候使用的那些难以操控的东西其实是咱们mock的对象,而咱们mock的对象则能够按照咱们的意愿返回一些值用于测试。通俗来说,Mock对象就是真实对象在咱们调试期间的测试品。

Mock对象建立的步骤:

  1. 使用一个接口来描述这个对象。

  2. 为产品代码实现这个接口。

  3. 以测试为目的,在mock对象中实现这个接口。

Mock对象示例:

/// <summary>
    ///帐户操做类
    /// </summary>
    public class AccountService
    {
        /// <summary>
        /// 接口地址
        /// </summary>
        public string Url { get; set; }

        /// <summary>
        /// Http请求帮助类
        /// </summary>
        public IHttpHelper HttpHelper { get; set; }
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="httpHelper"></param>
        public AccountService(IHttpHelper httpHelper)
        {
            HttpHelper = httpHelper;
        }

        #region 支付
        /// <summary>
        /// 支付
        /// </summary>
        /// <param name="json">支付报文</param>
        /// <param name="tranAmt">金额</param>
        /// <returns></returns>
        public bool Pay(string json)
        {            
            var result = HttpHelper.Post(json, Url);
            if (result == "SUCCESS")//这是咱们要测试的业务逻辑
            {
                return true;
            }
            return false;
        }
        #endregion

        #region 查询余额
        /// <summary>
        /// 查询余额
        /// </summary>
        /// <param name="account"></param>
        /// <returns></returns>
        public decimal? QueryAmt(string account)
        {
            var url = string.Format("{0}?account={1}", Url, account);

            var result = HttpHelper.Get(url);

            if (!string.IsNullOrEmpty(result))//这是咱们要测试的业务逻辑
            {
                return decimal.Parse(result);
            }
            return null;
        }
        #endregion

    }
/// <summary>
    /// Http请求接口
    /// </summary>
    public interface IHttpHelper
    {
        string Post(string json, string url);

        string Get(string url);
    }
/// <summary>
    /// HttpHelper
    /// </summary>
    public class HttpHelper:IHttpHelper
    {
        public string Post(string json, string url)
        {
            //假设这是真实的Http请求
            var result = string.Empty;
            return result;
        }

        public string Get(string url)
        {
            //假设这是真实的Http请求
            var result = string.Empty;
            return result;
        }

    }
/// <summary>
    /// Mock的 HttpHelper
    /// </summary>
    public class MockHttpHelper:IHttpHelper
    {
        public string Post(string json, string url)
        {
            //这是Mock的Http请求
            var result = "SUCCESS";
            return result;
        }

        public string Get(string url)
        {
            //这是Mock的Http请求
            var result = "0.01";
            return result;
        }

   }

     如上,咱们的AccountService的业务逻辑依赖于外部对象Http请求的返回值在真实的业务中咱们给AccountService注入真实的HttpHelper类,而在单元测试中咱们注入本身Mock的HttpHelper,咱们能够根据不一样的用例来模拟不一样的Http请求的返回值来测试咱们的AccountService的业务逻辑。

注意:记住,咱们要测试的是AccountService的业务逻辑:根据不一样http的请求(或传入不一样的参数)而返回不一样的结果,必定要弄明白本身要测的是什么!而无关的外部对象内的逻辑咱们并不关心,咱们只须要让它给咱们返回咱们想要的值,来验证咱们的业务逻辑便可

    关于Mock对象通常会使用Mock框架,关于Mock框架的使用,咱们将在下一篇文章中介绍。.net 平台经常使用的Mock框架有Moq,PhinoMocks,FakeItEasy等。

3.4 单元测试之代码覆盖率

​ 在作单元测试时,代码覆盖率经常被拿来做为衡量测试好坏的指标,甚至,用代码覆盖率来考核测试任务完成状况,好比,代码覆盖率必须达到80%或90%。因而乎,测试人员费尽心思设计案例覆盖代码。所以我认为用代码覆盖率来衡量是不合适的,咱们最根本的目的是为了提升咱们回归测试的效率,项目的质量不是吗?

结束语

    本篇文章主要介绍了单元测试的WWH,分享了咱们为何要作单元测试并简单介绍了单元测试的概念以及如何去作单元测试。固然,千万不要天真的觉得看了本篇文章就能作好单元测试,若是你的组织开始推动了单元测试,那么在推动的过程当中相信仍然会遇到许多问题(就像咱们遇到的,依赖外部对象问题,静态方法如何mock......)。如何更好的去作单元测试任重而道远。下一篇文章将针对咱们具体实施推动单元测试中遇到的一些问题,来讨论如何更好的作单元测试。如:如何破除依赖,如何编写可靠可维护的测试,以及如何面向测试进行程序的设计等。

​ 未完待续,敬请关注......

参考

《单元测试的艺术》

咱们作单元测试的经历

相关文章
相关标签/搜索