精华内容
下载资源
问答
  • 验收测试包括Alpha测试和Beta测试。 Alpha测试:是由用户在开发者的场所来进行的,在一个受控的环境中进行。 Beta测试:由软件的最终用户在一个或多个用户场所来进行的,开发者通常不在现场,用户记录测试中遇到的...

    分享一个大牛的人工智能教程。零基础!通俗易懂!风趣幽默!希望你也加入到人工智能的队伍中来!请点击http://www.captainbed.net

    1、单元测试:完成最小的软件设计单元(模块)的验证工作,目标是确保模块被正确的编码,使用过程设计描述作为指南,对重要的控制路径进行测试以发现模块内的错误。通常情况下是白盒的,对代码风格和规则、程序设计和结构、业务逻辑等进行静态测试,及早的发现和解决不易显现的错误。

    2、集成测试:通过测试发现与模块接口有关的问题。目标是把通过了单元测试的模块拿来,构造一个在设计中所描述的程序结构,应当避免一次性的集成(除非软件规模很小),而采用增量集成。

    自顶向下集成:模块集成的顺序是首先集成主模块,然后按照控制层次结构向下进行集成,隶属于主模块的模块按照深度优先或广度优先的方式集成到整个结构中去。

    自底向上集成:从原子模块开始来进行构造和测试,因为模块是自底向上集成的,集成时要求所有隶属于某个顶层的模块总是存在的,也不再有使用稳定测试桩的必要。

    3、系统测试:是基于系统整体需求说明书的黑盒类测试,应覆盖系统所有联合的部件。系统测试是针对整个产品系统进行的测试,目的是验证系统是否满足了需求规格的定义,找出与需求规格不相符合或与之矛盾的地方。系统测试的对象不仅仅包括需要测试的产品系统的软件,还要包含软件所依赖的硬件、外设甚至包括某些数据、某些支持软件及其接口等。因此,必须将系统中的软件与各种依赖的资源结合起来,在系统实际运行环境下来进行测试。

    4、回归测试:回归测试是指在发生修改之后重新测试先前的测试用例以保证修改的正确性。理论上,软件产生新版本,都需要进行回归测试,验证以前发现和修复的错误是否在新软件版本上再次出现。回归测试的目的在于验证以前出现过但已经修复好的缺陷不再重新出现。一般指对某已知修正的缺陷再次围绕它原来出现时的步骤重新测试。

    5、验收测试:验收测试是指系统开发生命周期方法论的一个阶段,这时相关的用户或独立测试人员根据测试计划和结果对系统进行测试和接收。它让系统用户决定是否接收系统。它是一项确定产品是否能够满足合同或用户所规定需求的测试。验收测试包括Alpha测试和Beta测试。

    Alpha测试:是由用户在开发者的场所来进行的,在一个受控的环境中进行。

    Beta测试:由软件的最终用户在一个或多个用户场所来进行的,开发者通常不在现场,用户记录测试中遇到的问题并报告给开发者,开发者对系统进行最后的修改,并开始准备发布最终的软件。

    展开全文
  • 单元测试

    2021-03-23 14:24:47
    单元测试(模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。  单元测试(模块测试)是...
  • 文章目录为什么不想写单元测试什么要写单元测试什么时候写单元测试怎么写单元测试什么不想写单元测试 单元测试太浪费时间了。 随着系统的复杂度增加,你的一次改动可能引发出5个bug,或者你的bug被发现的时间...

    为什么不想写单元测试

    • 单元测试太浪费时间了。
      随着系统的复杂度增加,你的一次改动可能引发出5个bug,或者你的bug被发现的时间延后了,堆积到了一起,那么一段时间后,别人加班半小时写单元测试,你会加班到天亮改BUG。
    • 有测试人员帮我测,我还写什么单元测试。
      测试分很多阶段,比如单元测试、集成测试、系统测试、验收测试等,只有单元测试属于开发人员的工作,单元测试是开发人员在知道代码内部逻辑的情况下,有目的的测试,为自己的代码编写单元测试并通过是对测试人员负责,更是对自己负责。
    • 代码编译通过就对了,不需要测试。
      趁早告别代码,代码不需要你。
    • 这是个老系统、这个部分代码不是我写的,它们本来就没有单元测试。
      如果你要修改这部分没有单元测试代码,那么首先编写单元测试将会是一个很好了解代码逻辑的过程,并且可以保证你的修改是可测试的,有了足够的单元测试,今后的修改、重构、扩展才会有最基本的保障。
    • 不知道怎么写单元测试。
      你需要学习。

    为什么要写单元测试

    • 让我们对自己的代码有信心。
      如果修改一个功能如果没有任何测试代码,只能靠接口测试,当业务逻辑相对简单的时候可能还比较可行,但当业务逻辑非常复杂流程很长的时候,可能一个很小的代码错误就会让这次精心设计的测试场景泡汤,又得重新来过,而这个很小的代码错误在跑整个流程之前可以通过一个简单的单元测试发现。如果没有单元测试,长此以往,我们总是觉得自己的代码可能有问题,但是又无从检查,如果修改多次仍然有问题,慢慢地就失去了耐心和信心。
    • 让BUG发现的时间提前。
      测试时会将一些边界条件作为测试用例,而这些边界条件在实际使用中可能会很晚才发现,到时候程序异常出现BUG,改动起来一定会比一开始就发现这些问题更费时间。如果基于TDD的模式开发,一些代码逻辑的BUG、边界条件的疏忽问题都会提前得到解决。
    • 为代码重构保驾护航。
      重构代码和重写代码的重要区别是:重写可能导致系统暂时不可用,但重构系统一定是随时可用的,那怎么保证重构过程中系统功能不受影响呢?单元测试可以为重构提供一定的帮助,如果我们用提取函数的手法重构了一段代码,直接跑原有的测试用例就可以保证重构没有破坏原来的逻辑结构。
    • 通过单元测试快速熟悉代码。
      单元测试中的用例可以说是一种很好的文档,通过查看单元测试可以很快了解代码逻辑,特别是一些边界条件和历史bug。
    • 优化设计。
      编写单元测试驱动开发从调用者的角度去设计代码,让代码易于调用和测试,特别是使用TDD测试驱动开发的方式,开发者会在开发中不断调整和重构,让代码有一个较好的结构和设计,并解除软件中的耦合。
    • 可快速持续回归。
      结合持续集成,任何代码的修改都可以快速回归,而不是需要通过接口测试覆盖可能修改的路径,后者太麻烦不知能,也不一定每次都能覆盖所有路径。

    什么时候写单元测试

    • 写代码之前。
      这正是TDD提倡的开发模式:在写任何业务代码之前,甚至都不用定义接口、方法,先写测试用例,如果测试用例不通过,那么回去修改业务代码,直到所有测试用例写完并通过,其中编译不通过也算一种测试用例不通过。这种方式的好处是测试代码和业务代码先后完成,在不断修改业务代码过程中,重构时刻进行,让代码有更好的结构,并保证代码可以满足所有测试用例。
    • 写代码的时候。
      先编写少量业务代码,然后编写单元测试,测试通过后再继续写业务代码,直到业务代码写完并且所有单元测试通过。这种方式和第一种时间上基本一致,但是侧重点不同,本方法有明显的的缺陷:如果没有完全了解需求,没有足够的测试用例,那么可能会在编写业务代码过程中对之前的逻辑进行修改,这可能会同时修改之前的测试用例及断言。而TDD也可以理解为Task-Driven Development,在开发之前要对问题进行分析并进行任务分解,是基于了解需求的情况下开发,所以重复工作相对较少。
    • 写完代码再写测试。
      这样的好处无非写业务代码的时候不考虑单元测试,不好的地方也比较明显:可能写业务逻辑比较任性,不会站在测试和调用者的角度去改善代码的设计;写完代码再写测试,可能会为了提高单元测试覆盖率而测试,并没有特别多有意义的测试;写完代码后的单元测试可能出现粒度较大的情况,导致测试之间的耦合度较高,可读性较差,可维护性不高;更有可能因为写完业务代码来不及写单元测试甚至懒得写单元测试而不写单元测试。

    综上所述,写单元测试最好的时机是写业务代码之前;如果做不到这样,可以先写部分业务代码,再写部分单元测试,两者几乎同时完成;相对不好的时机是写完整个业务代码之后,但对于没有单元测试的代码,可能只有通过这种方式增加单元测试。

    怎么写单元测试

    • 使用单元测试框架。
      Java中最流行的莫过于JUnit,这些工具远比自己写一个执行、验证流程来得方便,比如JUnit支持注解、允许忽略某些测试、允许特定顺序执行某些测试、支持初始化和清理、支持在不同运行环境中测试、可以通过Maven等构建工具来做持续集成和自动执行测试。
    • 强烈建议使用测试驱动开发模式(TDD)。
      TDD会专门在后面的一个小节总结。
    • 使它们短。
      简单明了,一个有意义的单元测试不需要太长。
    • 不要写重复代码。
      复制粘贴并不是好事,尽量使用setup/teardown或辅助方法减少重复代码。
    • 组合好过继承。
      base class可能包含一些常用代码,除非规定base class不能再添加功能,否则base class会越来越臃肿,难以管理和共享。
    • 快。
      尽量移除外部依赖,比如用h2内存数据库代替真实数据库、mock其他服务的接口、mock文件操作等,否则执行一次单元测试可能花费巨长的时间,这让测试和开发无法快速切换。
    • 评估代码覆盖率。
      高代码覆盖率并不能保证不出BUG,甚至不能保证代码逻辑是完美的,但绝对可以保证更多的代码被某一处执行过,至少在现有的测试用中不会抛出异常,断言关注的点也没有问题。所以写单元测试的时候结合jacoco、sonar或IDEA覆盖率工具评估覆盖率,可以查看自己是否遗漏了某些测试用例,是否有一些逻辑从未到达过。
    • 尽可能将测试数据外置化。
      JUnit测试可以通过@Parameterized将入参和断言等作为参数传入,当需要添加用例的时候就不要添加代码了,直接添加用例即可:
      package github.clyoudu.util;
       
      import org.junit.Assert;
      import org.junit.Test;
      import org.junit.runner.RunWith;
      import org.junit.runners.Parameterized;
       
      import java.util.Collection;
       
      /**
       * Create by IntelliJ IDEA
       *
       * @author chenlei
       * @dateTime 2019/8/6 20:23
       * @description FileUtil1Test
       */
      @RunWith(Parameterized.class)
      public class FileUtil1Test {
       
          private String path;
          private String expected;
       
          public FileUtil1Test(String path, String expected) {
              this.path = path;
              this.expected = expected;
          }
       
          @Parameterized.Parameters
          public static Collection<Object[]> getTestData() {
              return ArraysUtil.asList(new Object[][]{
                      {"///a//b//c/d/e.txt", "/a/b/c/d/e.txt"},
                      {"C:\\a\\\\b\\\\\\\\\\c\\d\\\\\\e.txt", "C:\\a\\b\\c\\d\\e.txt"}
              });
          }
       
          @Test
          public void test() {
              Assert.assertEquals(expected, FileUtil.formatFilePath(path));
          }
       
      }
      
    • 使用断言而不是打印语句。
      使用JUnit工具就是要通过断言来自动判断执行结果和期望是否一致,千万不要使用log打印到控制台来肉眼判断。
    • 生成具有确定性结果的测试。
      如果一个方法每次返回的结果是不可预期的,比如一个生成随机数的方法,那为这个方法编写单元测试几乎没有意义,因为不能得到可以预测的结果,这时候可以针对返回值的范围等作出断言,让返回值是有可比较的。
    • 不要忽略测试。
      测试跑不过了,发现是环境问题,直接加@Ignore注解忽略掉,这不是解决测试不通过的方法,应该从其他方面来让这个测试重新通过,比如剔除外部依赖、mock其他服务的接口等等。
    • 测试错误情景、边界情景和正常情景。
      比如雪球规定某个字段只能输入2-30个字符,那么长度小于2、大于30、2-30、2、30的字符串都应该被作为输入验证,同时null值也应当作为异常情景测试用例。
    • 设计测试。
      测试并不是不需要设计的,业务代码中的坏味道在测试代码中一样存在,需要清除这些坏味道,让测试代码理解和维护起来一样顺心顺手。

    如何编写优秀的单元测试

    • 编写可靠的测试
      • 依据实际情况合理地删除或修改单元测试: 如果确定是测试缺陷,而不是产品缺陷(被测试代码缺陷)时,需要立刻修改相关单元测试代码;如果被测试的产品代码的语义或者API变更导致测试失败,这时是需要修改测试,使用新的语义;如果看到测试名含义不清或者单元测试的可维护性差就应该在保证单元测试基本功能前提下修改测试名称或者重构测试;如果同一个功能多个单元测试,请删除重复测试。
      • 避免在单元测试代码中包含逻辑: 包含逻辑的测试是指测试代码中包含switch、if/else、for/while等控制流语句。这样的测试可读性差,代码脆弱,测试代码的复杂度高,容易包含缺陷,测试结果不容易重现。
      • 每个单元测试只测试一个关注点: 所谓的一个关注点就是指一个工作单元的一个最终结果:一个返回值、系统状态的一个改变、对第三方对象的一个调用。测试多个关注点一方面不利于测试命名,另一方面很多单元测试框架中,一个失败断言就会抛出一个特殊类型的异常,后面代码不会继续执行,这样不利于收集测试失败原因。
      • 区分单元测试和集成测试
      • 用代码审查确保代码覆盖率: 如果你做了代码审查、测试审查、确保测试优秀而且覆盖了所有代码,那么就可以避免犯简单愚蠢的错误,同时也可以从持续的学习中获益。
    • 编写可读的测试
      • 单元测试的命名标准: 合理地命名测试,主要目的是为了使后来的开发者从为了理解测试而阅读代码的负担中解脱出来。测试名应该包含三部分:被测试方法名、测试场景(即测试使用的条件)、预期行为(即被测试方法的最终结果)。
      • 单元测试中的变量命名规范: 单元测试除了主要的测试功能之外,它还为API提供某种形式的文档。通过合理命名变量,帮助阅读测试的人可以尽快理解你要验证什么(从而更加理解产品代码中想要实现什么功能)。
      • 断言和操作分离
      • 避免滥用setup和teardown(before和after): 比如在setup中准备stub和mock对象,这种情况就会导致阅读测试的人意识不到测试中使用了模拟对象,也不知道这些模拟对象预期是什么。
    • 编写可维护的测试
      • 只测试公共契约,避免测试私有或者受保护的方法: 私有方法可以看做是系统内部契约,这个内部契约是动态,在系统重构时可能会被随时修改,因此针对这些内部契约的单元测试也很可能会失败。而内部契约最终都会被一个公共契约(公共方法、整体功能)所调用,也就是说任何私有方法通常都是一个更大的工作单元的一部分。
      • 去除重复代码: 可以使用辅助方法或者setup来去除重复代码的问题
      • 实施测试隔离: 测试隔离是指每个测试都只生活在自己的小世界中,它与其他测试之间没有任何依赖关系,甚至不知道其他测试存在。
        几种常见的测试隔离的反模式:
        • 测试结果依赖测试执行的顺序
        • 测试调用其他测试方法
        • 测试中使用的共享资源(内存或外部资源)没有得到清理或回滚
      • 避免对不同关注点多次断言,尽量使用参数化测试或者对每个关注点设计单独的测试用例
      • 避免过度指定
        常见的过度指定例子
        • 对系统内部契约进行断言
        • 使用过多的模拟对象
        • 精确匹配
    展开全文
  • 单元测试什么

    2020-01-02 11:33:18
    单元测试是开发人员编写的、用于检测在特定条件下目标代码正确性的代码。单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体...

    单元测试是开发人员编写的、用于检测在特定条件下目标代码正确性的代码。单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

    单元测试位于测试金字塔的最底层,越向上反馈的时间越长,实现的成本也越高。

    测试的好处不仅仅是在编码时可以快速验证我们的程序是否满足预期,更大的好处是未来修改另一个功能时,可以帮助我们快速回归之前的所有测试,以确定此修改的影响范围。

    单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。例如,你可能把一个很大的值放入一个有序list 中去,然后确认该值出现在list 的尾部。或者,你可能会从字符串中删除匹配某种模式的字符,然后确认字符串确实不再包含这些字符了。

    单元测试是由程序员自己来完成,最终受益的也是程序员自己。程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。

    其实我们每天都在做单元测试。你写了一个函数,除了极简单的外,总是要执行一下,看看功能是否正常,有时还要想办法输出些数据,如弹出信息窗口什么的,这,也是单元测试,把这种单元测试称为临时单元测试。只进行了临时单元测试的软件,针对代码的测试很不完整,代码覆盖率要超过70%都很困难,未覆盖的代码可能遗留大量的细小的错误,这些错误还会互相影响,当BUG暴露出来的时候难于调试,大幅度提高后期测试和维护成本。进行充分的单元测试,是提高编码质量,降低开发成本的必由之路。

    要进行充分的单元测试,应专门编写测试代码,并与产品代码隔离。比较简单的办法是为产品工程建立对应的测试工程,为每个类建立对应的测试类,为每个函数(很简单的除外)建立测试函数。

    有一种看法是,只测试类的接口(公有函数),不测试其他函数,从面向对象角度来看,确实有其道理,但是,测试的目的是找错并最终排错,因此,只要是包含错误的可能性较大的函数都要测试,跟函数是否私有没有关系。对于C++来说,可以用一种简单的方法区隔需测试的函数:简单的函数如数据读写函数的实现在头文件中编写(inline函数),所有在源文件编写实现的函数都要进行测试(构造函数和析构函数除外)。

    使用效果

    单元测试会为我们的承诺做保证。编写单元测试就是用来验证这段代码的行为是否与我们期望的一致。有了单元测试,我们可以自信的交付自己的代码。

    什么时候测试?单元测试越早越好,早到什么程度?极限编程(Extreme Programming,或简称XP)讲究TDD,即测试驱动开发,先编写测试代码,再进行开发。在实际的工作中,可以不必过分强调先什么后什么,重要的是高效和感觉舒适。从经验来看,先编写产品函数的框架,然后编写测试函数,针对产品函数的功能编写测试用例,然后编写产品函数的代码,每写一个功能点都运行测试,随时补充测试用例。所谓先编写产品函数的框架,是指先编写函数空的实现,有返回值的直接返回一个合适值,编译通过后再编写测试代码,这时,函数名、参数表、返回类型都应该确定下来了,所编写的测试代码以后需修改的可能性比较小。

    由谁测试?单元测试与其他测试不同,单元测试可看作是编码工作的一部分,应该由程序员完成,也就是说,经过了单元测试的代码才是已完成的代码,提交产品代码时也要同时提交测试代码。

    关于桩代码,单元测试应避免编写桩代码。桩代码就是用来代替某些代码的代码,例如,产品函数或测试函数调用了一个未编写的函数,可以编写桩函数来代替该被调用的函数,桩代码也用于实现测试隔离。采用由底向上的方式进行开发,底层的代码先开发并先测试,可以避免编写桩代码,这样做的好处有:减少了工作量;测试上层函数时,也是对下层函数的间接测试;当下层函数修改时,通过回归测试可以确认修改是否导致上层函数产生错误。

    误解

    1、它浪费了太多的时间

    一旦编码完成,开发人员总是会迫切希望进行软件的集成工作,这样他们就能够看到实际的系统开始启动工作了。单元测试这样的活动也许会被看作是通往这个阶段点的道路上的障碍, 推迟了对整个系统进行联调这种真正有意思的工作启动的时间。

    系统能够正常工作的可能性是很小的,更多的情况是充满了各式各样的Bug。而且当这个系统投入使用时也无法确保它能够可靠运行。

    在实践工作中,进行了完整计划的单元测试和编写实际的代码所花费的精力大致上是相同的。一旦完成了这些单元测试工作,很多Bug将被纠正,在确信他们手头拥有稳定可靠的部件的情况下,开发人员能够进行更高效的系统集成工作。这才是真实意义上的进步,所以说完整计划下的单元测试是对时间的更高效的利用。

    2、它仅仅是证明这些代码做了什么

    这是那些没有首先为每个单元编写一个详细的规格说明而直接跳到编码阶段的开发人员提出的一条普遍的抱怨, 当编码完成以后并且面临代码测试任务的时候,他们就阅读这些代码并找出它实际上做了什么,把他们的测试工作基于已经写好的代码的基础上。

    如果他们首先写好一个详细的规格说明,测试能够以规格说明为基础。代码就能够针对它的规格说明,而不是针对自身进行测试。这样的测试能找到更多的编码错误,甚至是一些规格说明中的错误。好的规格说明可以使测试的质量更高,所以最后的结论是高质量的测试需要高质量的规格说明。

    这个过程的主要输入条件是要阅读那些程序代码和注释, 主要针对这个单元, 及调用它和被它调用的相关代码。画出流程图是非常有帮助的,你可以用手工或使用某种工具。可以组织对这个概要规格说明的走读(Review),以确保对这个单元的说明没有基本的错误, 有了这种最小程度的代码深层说明,就可以用它来设计单元测试了。

    3、我是个很棒的程序员, 我是不是可以不进行单元测试?

    每个人都会犯错误。真正的软件系统是非常复杂的。真正的软件系统不可以寄希望于没有进行广泛的测试和Bug修改过程就可以正常工作。

    编码不是一个可以一次性通过的过程。软件产品必须进行维护以对操作需求的改变作出反应, 并且要对最初的开发工作遗留下来的Bug进行修改。依靠那些原始作者进行修改吗?在开发人员做出修改后进行可重复的单元测试可以避免产生那些令人不快的负作用。

    4、集成测试将会抓住所有的Bug。

    规模越大的代码集成意味着复杂性就越高。如果软件的单元没有事先进行测试,开发人员很可能会花费大量的时间仅仅是为了使软件能够运行,而任何实际的测试方案都无法执行。

    一旦软件可以运行了,开发人员又要面对这样的问题:在考虑软件全局复杂性的前提下对每个单元进行全面的测试。这是一件非常困难的事情。

    最后的结果是测试将无法达到它所应该有的全面性。一些缺陷将被遗漏,并且很多Bug将被忽略过去。

    让我们类比一下,假设我们要清洗一台已经完全装配好的食物加工机器!无论你喷了多少水和清洁剂,一些食物的小碎片还是会粘在机器的死角位置,只有任其腐烂并等待以后再想办法。但如果这台机器是拆开的, 这些死角也许就不存在或者更容易接触到了,并且每一部分都可以毫不费力的进行清洗。

    成本效率

    无论什么时候作出修改都需要进行完整的回归测试,在生命周期中尽早地对软件产品进行测试将使效率和质量都得到最好的保证。Bug发现的越晚,修改它所需的费用就越高,因此从经济角度来看, 应该尽可能早的查找和修改Bug。在修改费用变的过高之前,单元测试是一个在早期抓住Bug的机会。

    相比后阶段的测试,单元测试的创建更简单,维护更容易,并且可以更方便的进行重复。

    结论

    经验表明一个尽责的单元测试方法将会在软件开发的某个阶段发现很多的Bug,并且修改它们的成本也很低。无论什么时候作出修改都要进行完整的回归测试,在生命周期中尽早地对软件产品进行测试将使效率和质量得到最好的保证。在提供了经过测试的单元的情况下,系统集成过程将会大大地简化。开发人员可以将精力集中在单元之间的交互作用和全局的功能实现上,而不是陷入充满很多Bug的单元之中不能自拔。

    优点

    1、它是一种验证行为。

    程序中的每一项功能都是测试来验证它的正确性。它为以后的开发提供支援。就算是开发后期,我们也可以轻松的增加功能或更改程序结构,而不用担心这个过程中会破坏重要的东西。而且它为代码的重构提供了保障。这样,我们就可以更自由的对程序进行改进。

    2、它是一种设计行为。

    编写单元测试将使我们从调用者观察、思考。特别是先写测试(test-first),迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。

    3、它是一种编写文档的行为。

    单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。

    4、它具有回归性。

    自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地的快速运行测试。

    范畴

    如果要给单元测试定义一个明确的范畴,指出哪些功能是属于单元测试,这似乎很难。但下面讨论的四个问题,基本上可以说明单元测试的范畴,单元测试所要做的工作。

    1、 它的行为和我期望的一致吗?

    这是单元测试最根本的目的,我们就是用单元测试的代码来证明它所做的就是我们所期望的。

    2、 它的行为一直和我期望的一致吗?

    编写单元测试,如果只测试代码的一条正确路径,让它正确走一遍,并不算是真正的完成。软件开发是一项复杂的工程,在测试某段代码的行为是否和你的期望一致时,你需要确认:在任何情况下,这段代码是否都和你的期望一致;譬如参数很可疑、硬盘没有剩余空间、缓冲区溢出、网络掉线的时候。

    3、 我可以依赖单元测试吗?

    不能依赖的代码是没有多大用处的。既然单元测试是用来保证代码的正确性,那么单元测试也一定要值得依赖。

    4、 单元测试说明我的意图了吗?

    单元测试能够帮我们充分了解代码的用法,从效果上而言,单元测试就像是能执行的文档,说明了在你用各种条件调用代码时,你所能期望这段代码完成的功能。

    不写的借口

    到这里,我们已经列举了使用单元测试的种种理由。也许,每个人都同意,是的,该做更多的测试。这种人人同意的事情还多着呢,是的,该多吃蔬菜,该戒烟,该多休息,该多锻炼……这并不意味着我们中的所有人都会这么去做,不是吗?

    1、 编写单元测试太花时间了。

    我们知道,在开发时越早发现BUG,就能节省更多的时间,降低更多的风险。

    如果你仍然认为在编写产品代码的时候,还是没有时间编写测试代码,那么请先考虑下面这些问题:

    1)、对于所编写的代码,你在调试上面花了多少时间。

    2)、对于以前你自认为正确的代码,而实际上这些代码却存在重大的bug,你花了多少时间在重新确认这些代码上面。

    3)、对于一个别人报告的bug,你花了多少时间才找出导致这个bug 的源码位置。

    回答完这些问题,你一定不再以“太花时间”作为拒绝单元测试的借口。

    2、 运行测试的时间太长了。

    合适的测试是不会让这种情况发生的。实际上,大多数测试的执行都是非常快的,因此你在几秒之内就可以运行成千上万个测试。但是有时某些测试会花费很长的时间。这时,需要把这些耗时的测试和其他测试分开。通常可以每天运行这种测试一次,或者几天一次。

    3、 测试代码并不是我的工作。

    你的工作就是保证代码能够正确的完成你的行为,恰恰相反,测试代码正是你不可缺少的工作。

    4、 但是这些代码都能够编译通过。

    我们前面已经说过,代码通过编译只是验证它的语法通过。但并不能保证它的行为就一定正确。

    5、 公司请我来是为了写代码,而不是写测试。

    公司付给你薪水是为了让你编写产品代码,而单元测试大体上是一个工具,是一个和编辑器、开发环境、编译器等处于同一位置的工具。

    6、 我的公司并不会让我在真实系统中运行单元测试。

    我们所讨论的只是针对开发者的单元测试。也就是说,如果你可以在其他的环境下(例如在正式的产品系统中)运行这些测试的话,那么它们就不再是单元测试,而是其他类型的测试了。实际上,你可以在你的本机运行单元测试,使用你自己的数据库,或者使用mock 对象。

    代码编写

    本文以C++为例,后半部分所介绍的单元测试工具也只介绍C++单元测试工具。下面的示例代码的开发环境是VC6.0。

    产品类

    class CMyClass{

    public:

    int Add(int i,int j);

    CMyClass();

    virtual ~CMyClass();

    private:

    int mAge; //年龄

    CString mPhase; //年龄阶段,如"少年","青年"

    };

    建立对应的测试类CMyClassTester,为了节约编幅,只列出源文件的代码:

    void CMyClassTester::CaseBegin()

    {

    //pObj是CMyClassTester类的成员变量,是被测试类的对象的指针,

    //为求简单,所有的测试类都可以用pObj命名被测试对象的指针。

    pObj = new CMyClass();

    }

    void CMyClassTester::CaseEnd()

    {

    delete pObj;

    }

    测试类的函数CaseBegin()和CaseEnd()建立和销毁被测试对象,每个测试用例的开头都要调用CaseBegin(),结尾都要调用CaseEnd()。

    产品函数

    接下来,我们建立示例的产品函数:

    int CMyClass::Add(int i,int j)

    {

    return i+j;

    }

    和对应的测试函数:

    void CMyClassTester::Add_int_int()

    {

    }

    把参数表作为函数名的一部分,这样当出现重载的被测试函数时,测试函数不会产生命名冲突。下面添加测试用例:

    void CMyClassTester::Add_int_int()

    {

    //第一个测试用例

    CaseBegin();{ //1

    int i = 0; //2

    int j = 0; //3

    int ret = pObj->Add(i,j); //4

    ASSERT(ret == 0); //5

    }CaseEnd(); //6

    }

    测试用例

    下面说说测试用例、输入数据及预期输出。输入数据是测试用例的核心,对输入数据的定义是:被测试函数所读取的外部数据及这些数据的初始值。外部数据是对于被测试函数来说的,实际上就是除了局部变量以外的其他数据,老纳把这些数据分为几类:参数、成员变量、全局变量、IO媒体。IO媒体是指文件、数据库或其他储存或传输数据的媒体,例如,被测试函数要从文件或数据库读取数据,那么,文件或数据库中的原始数据也属于输入数据。一个函数无论多复杂,都无非是对这几类数据的读取、计算和写入。预期输出是指:返回值及被测试函数所写入的外部数据的结果值。返回值就不用说了,被测试函数进行了写操作的参数(输出参数)、成员变量、全局变量、IO媒体,它们的预期的结果值都是预期输出。一个测试用例,就是设定输入数据,运行被测试函数,然后判断实际输出是否符合预期。下面举一个与成员变量有关的例子:

    产品函数

    void CMyClass::Grow(int years)

    {

    mAge += years;

    if(mAge < 10)

    mPhase = "儿童";

    else if(mAge <20)

    mPhase = "少年";

    else if(mAge <45)

    mPhase = "青年";

    else if(mAge <60)

    mPhase = "中年";

    else

    mPhase = "老年";

    }

    测试函数中的一个测试用例:

    CaseBegin();{

    int years = 1;

    pObj->mAge = 8;

    pObj->Grow(years);

    ASSERT( pObj->mAge == 9 );

    ASSERT( pObj->mPhase == "儿童" );

    }CaseEnd();

    在输入数据中对被测试类的成员变量mAge进行赋值,在预期输出中断言成员变量的值。现在可以看到老纳所推荐的格式的好处了吧,这种格式可以适应很复杂的测试。在输入数据部分还可以调用其他成员函数,例如:执行被测试函数前可能需要读取文件中的数据保存到成员变量,或需要连接数据库,老纳把这些操作称为初始化操作。例如,上例中 ASSERT( ...)之前可以加pObj->OpenFile();。为了访问私有成员,可以将测试类定义为产品类的友元类。例如,定义一个宏:

    #define UNIT_TEST(cls) friend class cls##Tester;

    然后在产品类声明中加一行代码:UNIT_TEST(ClassName)。

    测试用例设计

    测试用例的核心是输入数据。预期输出是依据输入数据和程序功能来确定的,也就是说,对于某一程序,输入数据确定了,预期输出也就可以确定了,至于生成/销毁被测试对象和运行测试的语句,是所有测试用例都大同小异的,因此,我们讨论测试用例时,只讨论输入数据。

    前面说过,输入数据包括四类:参数、成员变量、全局变量、IO媒体,这四类数据中,只要所测试的程序需要执行读操作的,就要设定其初始值,其中,前两类比较常用,后两类较少用。显然,把输入数据的所有可能取值都进行测试,是不可能也是无意义的,我们应该用一定的规则选择有代表性的数据作为输入数据,主要有三种:正常输入,边界输入,非法输入,每种输入还可以分类,也就是平常说的等价类法,每类取一个数据作为输入数据,如果测试通过,可以肯定同类的其他输入也是可以通过的。下面举例说明:

    正常输入

    例如字符串的Trim函数,功能是将字符串前后的空格去除,那么正常的输入可以有四类:前面有空格;后面有空格;前后均有空格;前后均无空格。

    边界输入

    上例中空字符串可以看作是边界输入。

    再如一个表示年龄的参数,它的有效范围是0-100,那么边界输入有两个:0和100。

    非法输入

    非法输入是正常取值范围以外的数据,或使代码不能完成正常功能的输入,如上例中表示年龄的参数,小于0或大于100都是非法输入,再如一个进行文件操作的函数,非法输入有这么几类:文件不存在;目录不存在;文件正在被其他程序打开;权限错误。

    如果函数使用了外部数据,则正常输入是肯定会有的,而边界输入和非法输入不是所有函数都有。一般情况下,即使没有设计文档,考虑以上三种输入也可以找出函数的基本功能点。实际上,单元测试与代码编写是“一体两面”的关系,编码时对上述三种输入都是必须考虑的,否则代码的健壮性就会成问题。

    白盒覆盖

    上面所说的测试数据都是针对程序的功能来设计的,就是所谓的黑盒测试。单元测试还需要从另一个角度来设计测试数据,即针对程序的逻辑结构来设计测试用例,就是所谓的白盒测试。在老纳看来,如果黑盒测试是足够充分的,那么白盒测试就没有必要,可惜“足够充分”只是一种理想状态,例如:真的是所有功能点都测试了吗?程序的功能点是人为的定义,常常是不全面的;各个输入数据之间,有些组合可能会产生问题,怎样保证这些组合都经过了测试?难于衡量测试的完整性是黑盒测试的主要缺陷,而白盒测试恰恰具有易于衡量测试完整性的优点,两者之间具有极好的互补性,例如:完成功能测试后统计语句覆盖率,如果语句覆盖未完成,很可能是未覆盖的语句所对应的功能点未测试。

    白盒测试针对程序的逻辑结构设计测试用例,用逻辑覆盖率来衡量测试的完整性。逻辑单位主要有:语句、分支、条件、条件值、条件值组合,路径。语句覆盖就是覆盖所有的语句,其他类推。另外还有一种判定条件覆盖,其实是分支覆盖与条件覆盖的组合,在此不作讨论。跟条件有关的覆盖就有三种,解释一下:条件覆盖是指覆盖所有的条件表达式,即所有的条件表达式都至少计算一次,不考虑计算结果;条件值覆盖是指覆盖条件的所有可能取值,即每个条件的取真值和取假值都要至少计算一次;条件值组合覆盖是指覆盖所有条件取值的所有可能组合。老纳做过一些粗浅的研究,发现与条件直接有关的错误主要是逻辑操作符错误,例如:||写成&&,漏了写!什么的,采用分支覆盖与条件覆盖的组合,基本上可以发现这些错误,另一方面,条件值覆盖与条件值组合覆盖往往需要大量的测试用例,因此,在老纳看来,条件值覆盖和条件值组合覆盖的效费比偏低。效费比较高且完整性也足够的测试要求是这样的:完成功能测试,完成语句覆盖、条件覆盖、分支覆盖、路径覆盖。做过单元测试的朋友恐怕会对老纳提出的测试要求给予一个字的评价:晕!或者两个字的评价:狂晕!因为这似乎是不可能的要求,要达到这种测试完整性,其测试成本是不可想象的,不过,出家人不打逛语,老纳之所以提出这种测试要求,是因为利用一些工具,可以在较低的成本下达到这种测试要求,后面将会作进一步介绍。

    测试工具

    现在开始介绍单元测试工具,分别按编程语言进行分组介绍。

    C/C++

    CppUnit

    首先是CppUnit,这是C++单元测试工具的鼻祖,免费的开源的单元测试框架。建议读一下Cpluser 所作的《CppUnit测试框架入门》。

    C++Test

    然后介绍C++Test,这是Parasoft公司的产品。[C++Test是一个功能强大的自动化C/C++单元级测试工具,可以自动测试任何C/C++函数、类,自动生成测试用例、测试驱动函数或桩函数,在自动化的环境下极其容易快速的将单元级的测试覆盖率达到100%]。

    gtest

    gtest测试框架是在不同平台上(Linux,Mac OS X,Windows,Cygwin,Windows CE和Symbian)为编写C++测试而生成的。它是基于xUnit架构的测试框架,支持自动发现测试,丰富的断言集,用户定义的断言,death测试,致命与非致命的失败,类型参数化测试,各类运行测试的选项和XML的测试报告。

    应用

    极限编程

    单元测试是极限编程的基础,依赖于自动化的单元测试框架。

    极限编程创建单元测试用于测试驱动开发。首先,开发人员编写单元测试用于展示软件需求或者软件缺陷。因为需求尚未实现或者现有代码中存在软件缺陷,这些测试会失败。然后,开发人员遵循测试要求编写最简单的代码去满足它,直到测试得以通过。

    系统中大多数代码都经过单元测试,但并非所有代码路径都必需单元测试。极限编程强调“测试所有可能中断”的策略,而传统方法是“测试所有执行路径”。这使得极限编程开发人员比传统开发少写单元测试,但这并不是问题。不争的事实是传统方法很少完全遵循完整地测试所有执行路径的要求。极限编程相互地认识到测试很少能完备,提供了如何有效地将有限资源集中投入可花费的代价到问题关键的导引。

    技术

    单元测试通常情况下自动进行,但也可被手动执行。单元测试的目标是隔离程序单元并验证其正确性。自动执行使目标达成更有效,也可获得本文上述单元测试收益。

    借助于自动化测试框架,开发人员可以抓住关键进行编码并通过测试去验证程序单元的正确性。在测试案例执行期间,框架通过日志记录了所有失败的测试准则。很多测试框架可以自动标记和提交失败的测试案例总结报告。根据失败的程度不同,框架可以中止后续测试。

    总体说来,单元测试会激发程序员创造解耦的和内聚的代码体。单元测试实践有利于促进健康的软件开发习惯。设计模式、单元测试和重构经常一起出现在工作中,借助于它们,开发人员可以生产出最为完美的解决方案。

    GTEST

    When using googletest, you start by writing assertions, which are statements that check whether a condition is true. An assertion's result can be success, nonfatal failure, or fatal failure. If a fatal failure occurs, it aborts the current function; otherwise the program continues normally.

    Tests use assertions to verify the tested code's behavior. If a test crashes or has a failed assertion, then it fails; otherwise it succeeds.

    A test suite contains one or many tests. You should group your tests into test suites that reflect the structure of the tested code. When multiple tests in a test suite need to share common objects and subroutines, you can put them into a test fixture class.

    A test program can contain multiple test suites.

    googletest assertions are macros that resemble function calls. You test a class or function by making assertions about its behavior. When an assertion fails, googletest prints the assertion's source file and line number location, along with a failure message. You may also supply a custom failure message which will be appended to googletest's message.

    Basic Assertions

    These assertions do basic true/false condition testing.

    Fatal assertion

    Nonfatal assertion

    Verifies

    ASSERT_TRUE(condition);

    EXPECT_TRUE(condition);

    condition is true

    ASSERT_FALSE(condition);

    EXPECT_FALSE(condition);

    condition is false

    Remember, when they fail, ASSERT_* yields a fatal failure and returns from the current function, while EXPECT_* yields a nonfatal failure, allowing the function to continue running. In either case, an assertion failure means its containing test fails.

    Binary Comparison

    This section describes assertions that compare two values.

    Fatal assertion

    Nonfatal assertion

    Verifies

    ASSERT_EQ(val1, val2);

    EXPECT_EQ(val1, val2);

    val1 == val2

    ASSERT_NE(val1, val2);

    EXPECT_NE(val1, val2);

    val1 != val2

    ASSERT_LT(val1, val2);

    EXPECT_LT(val1, val2);

    val1 < val2

    ASSERT_LE(val1, val2);

    EXPECT_LE(val1, val2);

    val1 <= val2

    ASSERT_GT(val1, val2);

    EXPECT_GT(val1, val2);

    val1 > val2

    ASSERT_GE(val1, val2);

    EXPECT_GE(val1, val2);

    val1 >= val2

    Value arguments must be comparable by the assertion's comparison operator or you'll get a compiler error. We used to require the arguments to support the << operator for streaming to an ostream, but it's no longer necessary. If << is supported, it will be called to print the arguments when the assertion fails; otherwise googletest will attempt to print them in the best way it can. 

     
     

    展开全文
  • 单元测试1-为什么需要单元测试

    千次阅读 2016-10-10 11:35:11
    什么需要单元测试  软件开发的标准过程包括以下几个阶段:『需求分析阶段』→『设计阶段』→『实现阶段』→『测试阶段』→『发布』。其中测试阶段通过人工或者自动手段来运行或测试某个系统的过程,其目的...
    为什么需要单元测试 
    

       软件开发的标准过程包括以下几个阶段:『需求分析阶段』→『设计阶段』→『实现阶段』→『测试阶段』→『发布』。其中测试阶段通过人工或者自动手段来运行或测试某个系统的过程,其目的在于检验它是否满足规定的需求或弄清预期结果与实际结果之间的差别。测试过程按4个步骤进行,即单元测试、集成测试、系统测试及发版测试。其中功能测试主要检查已实现的软件是否满足了需求规格说明中确定了的各种需求,以及软件功能是否完全、正确。系统测试主要对已经过确认的软件纳入实际运行环境中,与其他系统成份组合在一起进行测试。单元测试、集成测试由开发人员进行,是我们关注的重点,下文对两者进行详细说明。

    单元测试

       单元测试是开发者编写的一小段代码,用于检验目标代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试用于判断某个特定条件或特定场景下某个特定函数的行为。例如,用户可能把一个很大的值放入一个有序List中,然后确认该值出现在List 的尾部。或者,用户可能会从字符串中删除匹配某种模式的字符,然后确认字符串确实不再包含这些字符了。
       单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。
       在一般情况下,一个功能模块往往会调用其他功能模块完成某项功能,如业务层的业务类可能会调用多个DAO完成某项业务。对某个功能模块进行单元测试时,我们希望屏蔽对外在功能模块的依赖,以便将焦点放在目标功能模块的测试上。这时模拟对象将是最有力的工具,它根据外在模块的接口模拟特定操作行为,这样单元测试就可以在假设关联模块正确工作的情况下验证本模块逻辑的正确性了。


    集成测试

      单元测试和开发工作是并驾齐驱的工作,甚至是前置性的工作。除了一些显而易见的功能外,大部分功能(类的方法)都必须进行单元测试,通过单元测试可以保障功能模块的正确性。而集成测试则是在功能模块开发完成后,为验证功能模块之间匹配调用的正确性而进行的测试。在单元测试时,往往需要通过模拟对象屏蔽外在模块的依赖,而集成测试恰恰是要验证模块之间集成后的正确性。
       举个例子,当对UserService这个业务层的类进行单元测试时,可以通过创建UserDao、LoginLogDao模拟对象,在假设DAO类正确工作的情况下对UserService进行测试。而对UserService进行集成测试时,则应该注入真实的UserDao和LoginLogDao进行测试。
       所以一般来讲,集成测试面向的层面要更高一些,一般对业务层和Web层进行集成测试,单元测试则面向一些功能单一的类(如字符串格式化工具类、数据计算类)。当然,我们可能对某一个类既进行单元测试又进行集成测试,如UserService在模块开发期间进行单元测试,而在关联的DAO类开发完成后,再进行集成测试。

    测试好处

        在编写代码的过程中,一定会反复调试保证它能够编译通过。但代码通过编译,只是说明了它的语法正确。无法保证它的语义也一定正确,没有任何人可以轻易承诺这段代码的行为一定是正确的。幸运的是,单元测试会为我们的承诺做保证。编写单元测试就是用来验证这段代码的行为是否与我们期望的一致。有了单元测试,我们可以自信地交付自己的代码,减少后顾之忧。总之进行单元测试,会带来以下好处:
    • 软件质量最简单、最有效的保证;
    • 是目标代码最清晰、最有效的文档;
    • 可以优化目标代码的设计;
    • 是代码重构的保障;
    • 是回归测试和持续集成的基石。


    单元测试之误解

       认为单元测试影响开发进度,一是借口,拒绝对单元测试相关知识进行学习(单元测试,代码重构,版本管理是开发人员的必备);二是单元测试是“先苦后甜”,刚开始搭建环境,引入额外工作,看似“影响进度”,但长远来看,由于程序质量提升、代码返工减少、后期维护工作量缩小、项目风险降低,从而在整体上赢了回来。

    误解1:影响开发进度

       一旦编码完成,开发人员总是会迫切希望进行软件的集成工作,这样他们就能够看到系统实际运行效果。这在外表上看来好像加快进度,而像单元测试这样的活动被看作是影响进度原因之一,推迟了对整个系统进行集成测试的时间。
    在实践中,这种开发步骤常常会导致这样的结果:软件甚至无法运行。更进一步的结果是大量的时间将被花费在跟踪那些包含在独立单元里的简单Bug上面,在个别情况下,这些Bug也许是琐碎和微不足道的,但是总的来说,它们会导致推迟软件产品交付的时间,而且也无法确保它能够可靠运行。
       在实际工作中,进行了完整计划的单元测试和编写实际的代码所花费的精力大致上是相同的。一旦完成了这些单元测试工作,很多Bug将被纠正,开发人员能够进行更高效的系统集成工作。这才是真实意义上的进步,所以说完整计划下的单元测试是对时间的更高效利用。

    误解2:增加开发成本

       如果不重视程序中那些未被发现的Bug可能带来的后果。这种后果的严重程度可以从一个Bug引起的用户使用不便到系统崩溃。这种后果可能常常会被软件的开发人员所忽视,这种情况会长期损害软件开发商的声誉,并且会对未来的市场产生负面影响。相反地,一个可靠的软件系统的良好的声誉将有助于一个软件开发商获取未来的市场。
    很多研究成果表明,无论什么时候作出修改都要进行完整的回归测试,在生命周期中尽早地对软件产品进行测试将使效率和质量得到最好的保证。Bug发现得越晚,修改它所需的费用就越高,因此从经济角度来看,应该尽可能早地查找和修改Bug。而单元测试就是一个在早期抓住Bug的机会。
       相比后阶段的测试,单元测试的创建更简单,且维护更容易,同时可以更方便地进行重构。从全程的费用来考虑,相比起那些复杂且旷日持久的集成测试,或是不稳定的软件系统来说,单元测试所需的费用是很低的。

    误解3:我是个编程高手,无须进行单元测试

       在每个开发团队中都至少有一个这样的开发人员,他非常擅长于编程,他开发的软件总是在第一时间就可以正常运行,因此不需要进行测试。你是否经常听到这样的借口?在现实世界里,每个人都会犯错误。即使某个开发人员可以抱着这种态度在很少的一些简单程序中应付过去,但真正的软件系统是非常复杂的。真正的软件系统不可以寄希望于没有进行广泛的测试和Bug修改过程就可以正常工作。编码不是一个可以一次性通过的过程。在现实世界中,软件产品必须进行维护以对操作需求的改变作出及时响应,并且要对最初的开发工作遗留下来的Bug进行修改。你希望依靠那些原始作者进行修改吗?这些制造出未经测试的代码的资深工程师们还会继续在其他地方制造这样的代码。在开发人员做出修改后进行可重复的单元测试,可以避免产生那些令人不快的负作用。

    误解4:测试人员会测出所有Bug

       一旦软件可以运行了,开发人员又要面对这样的问题:在考虑软件全局复杂性的前提下对每个单元进行全面的测试。这是一件非常困难的事情,甚至在创造一种单元调用的测试条件时,要全面考虑单元被调用时的各种入口参数。在软件集成阶段,对单元功能全面测试的复杂程度远远超过独立进行的单元测试过程。
       最后的结果是测试将无法达到它所应该有的全面性。一些缺陷将被遗漏,并且很多Bug将被忽略过去。让我们类比一下,假设我们要清理一台电脑主机中的灰尘,如果没有把主机中各个部件(显卡、内存等)拆开,无论你用什么工具,一些灰尘还会隐藏在主机的某些角落无法清理。但我们换个角度想想,如果把主机每个部件一一拆开,这些死角中的灰尘就容易被发现和接触到了,并且每一部件的灰尘都可以毫不费力地进行清理。

    单元测试之症结

       测试在软件开发过程中一直都是备受关注的,测试不仅仅局限于软件开发中的一个阶段,它已经开始贯穿于整个软件开发过程。大家普遍认识到,如果测试能在开发阶段进行有效执行,程序的Bug就会被及早发现,其质量就能得到有效的保证,从而减少软件开发总成本。但是,相对于测试这个词的流行程度而言,大家对单元测试的认知普遍存在一些偏差,特别是一些程序员很容易陷入一些误区,导致了测试并没有在他们所在的开发项目中起到有效的作用。下面对一些比较具有代表性的症结进行剖析,并对于测试背后所蕴含的一些设计思考进行阐述,希望能够起到抛砖引玉的作用。

    症结1:使用System.out.print跟踪和运行程序就够了

       这个症结可以说是程序员的一种通病,认为使用System.out.print就可以确保编写代码的正确性,无须编写测试用例,他们觉得编写用例是在“浪费时间”。使用System.out.print输出结果,以肉眼观察这种刀耕火种的方式进行测试,不仅效率低下,而且容易出错。

    症结2:使用System.out.print跟踪和运行程序就够了

       在编码的时候,确实存在一些看起来比较难测试的代码,但是并非无法测试。并且在大多数情况下,还是由于被测试的代码在设计时没有考虑到可测试性的问题。编写程序不仅与第三方一些框架耦合过紧,而且过于依赖其运行环境,从而表现出被测试的代码本身很难测试。

    症结3:测试代码可以随意写

       编写测试代码时抱着一种随意的态度,没有弄清测试的真正意图。编写测试代码只是为了应付任务而已,先编写程序实现代码,然后才去编写一些单元测试。表现出来的结果是测试过于简单,只走形式和花架,将大量Bug传递给系统测试人员。

    症结4:不关心测试环境

      手工搭建测试环境,测试数据,造成维护困难,占据了大量时间,严重影响效率。对测试产 生的“垃圾”不清除,不处理。造成测试不能重复进行,导致脆弱的测试,需要维护好测试环境,做一个“低碳环保”的测试者。

    症结5:测试环境依赖性大

       测试环境依赖性大,没有有效隔离测试目标及其依赖环境,一是使测试不聚焦;二是常因依赖环境的影响造成失败;三是因依赖环境太厚重从而降低测试的效率(如依赖数据库或依赖网络资源,如邮件系统、Web服务)。

    单元测试基本概念

    被测系统:SUT(System Under Test)


       被测系统(System under test,SUT)表示正在被测试的系统,目的是测试系统能否正确操作。这一词语常用于软件测试中。软件系统测试的一个特例是对应用软件的测试,称为被测应用程序(application under test,AUT)。
       SUT也表明软件已经到了成熟期,因为系统测试在测试周期中是集成测试的后一阶段。

    测试替身:Test Double

       在单元测试时,使用Test Double减少对被测对象的依赖,使得测试更加单一。同时,让测试案例执行的时间更短,运行更加稳定,同时能对SUT内部的输入输出进行验证,让测试更加彻底深入。但是,Test Double也不是万能的,Test Double不能被过度使用,因为实际交付的产品是使用实际对象的,过度使用Test Double会让测试变得越来越脱离实际。
    要理解测试替身,需要了解一下Dummy Objects、Test Stub、Test Spy、Fake Object
       这几个概念,下面我们对这些概念分别进行说明。

    Dummy Objects

        Dummy Objects泛指在测试中必须传入的对象,而传入的这些对象实际上并不会产生任何作用,仅仅是为了能够调用被测对象而必须传入的一个东西。

    Test Stub

        测试桩是用来接受SUT内部的间接输入(indirect inputs),并返回特定的值给SUT。可以理解Test Stub是在SUT内部打的一个桩,可以按照我们的要求返回特定的内容给SUT,Test Stub的交互完全在SUT内部,因此,它不会返回内容给测试案例,也不会对SUT内部的输入进行验证。

    Test Spy

       Test Spy像一个间谍,安插在了SUT内部,专门负责将SUT内部的间接输出(indirect outputs)传到外部。它的特点是将内部的间接输出返回给测试案例,由测试案例进行验证,Test Spy只负责获取内部情报,并把情报发出去,不负责验证情报的正确性。

    Mock Object

       Mock Object和Test Spy有类似的地方,它也是安插在SUT内部,获取到SUT内部的间接输出(indirect outputs),不同的是,Mock Object还负责对情报(intelligence)进行验证,总部(外部的测试案例)信任Mock Object的验证结果。

    Fake Object

       经常,我们会把Fake Object和Test Stub搞混,因为它们都和外部没有交互,对内部的输入输出也不进行验证。不同的是,Fake Object并不关注SUT内部的间接输入(indirect inputs)或间接输出(indirect outputs),它仅仅是用来替代一个实际的对象,并且拥有几乎和实际对象一样的功能,保证SUT能够正常工作。实际对象过分依赖外部环境,Fake Object可以减少这样的依赖。

    测试夹具:Test Fixture

        所谓测试夹具(Fixture),就是测试运行程序(test runner)会在测试方法之前自动初始化、回收资源的工作。JUnit4之前是通过setUp、TearDown方法完成。在JUnit4中,仍然可以在每个测试方法运行之前初始化字段和配置环境,当然也是通过注解完成。在JUnit4中,通过@Befroe替代setUp方法;@After替代tearDown方法。在一个测试类中,甚至可以使用多个@Before来注解多个方法,这些方法都是在每个测试之前运行。说明一点,@Before是在每个测试方法运行前均初始化一次,同理@Ater是在每个测试方法运行完毕后均执行一次。也就是说,经这两个注解的初始化和注销,可以保证各个测试之间的独立性而互不干扰,它的缺点是效率低。另外,不需要在超类中显式调用初始化和清除方法,只要它们不被覆盖,测试运行程序将根据需要自动调用这些方法。超类中的@Before方法在子类的@Before方法之前调用(与构造函数调用顺序一致),@After方法是子类在超类之前运行。
        一个测试用例可以包含若干个打上@Test注解的测试方法,测试用例测试一个或多个类API接口的正确性,当然在调用类API时,需要事先创建这个类的对象及一些关联的对象,这组对象就称为测试夹具(Fixture),相当于测试用例的“工作对象”。
        前面讲过,一个测试用例类可以包含多个打上@Test注解的测试方法,在运行时,每个测试方法都对应一个测试用例类的实例。当然,用户可以在具体的测试方法里声明并实例化业务类的实例,在测试完成后销毁它们。但是,这么一来就要在每个测试方法中都重复这些代码,因为TestCase实例依照以下步骤运行。

    •   创建测试用例的实例。
    •   使用注解@Before注解修饰用于初始化夹具的方法。
    •   使用注解@After注解修饰用于注销夹具的方法。
    •   保证这两种方法都使用 public void 修饰,而且不能带有任何参数。


    TestCase实例运行过程如下图所示:



       之所以每个测试方法都需要按以上流程运行,是为了防止测试方法相互之间的影响,因为在同一个测试用例类中不同测试方法可能会使用到相同的测试夹具,前一个测试方法对测试夹具的更改会影响后一个测试方法的现场。而通过如上的运行步骤后,因为每个测试方法运行前都重建运行环境,所以测试方法相互之间就不会有影响了。
       可是,这种夹具设置方式还是引来了批评,因为它效率低下,特别是在设置 Fixture 非常耗时的情况下(例如设置数据库链接)。而且对于不会发生变化的测试环境或者测试数据来说,是不会影响到测试方法的执行结果的,也就没有必要针对每一个测试方法重新设置一次夹具。因此在 JUnit 4 中引入了类级别的夹具设置方法,编写规范说明如下。
    •   创建测试用例的实例。
    •   使用注解BeforeClass 修饰用于初始化夹具的方法。
    •   使用注解AfterClass 修饰用于注销夹具的方法。
    •   保证这两种方法都使用 public static void 修饰,而且不能带有任何参数。


       类级别的夹具仅会在测试类中所有测试方法执行之前执行初始化,并在全部测试方法测试完毕之后执行注销方法,如图16-2所示。

    测试用例:Test Case

       有了测试夹具,就可以开始编写测试用例的测试方法了。当然也可以不需要测试夹具而直接编写测试用例方法。
       在JUnit 3中,测试方法都必须以test为前缀,且必须是public void的,JUnit 4之后,就没有这个限制,只要在每个测试方法标注@Test注解,方法签名可以是任意取名。



      可以在一个测试用例中添加多个测试方法,运行器为每个方法生成一个测试用例实例并分别运行。

    测试套件:Test Suite

       如果每次只能运行一个测试用例,那么又陷入了传统测试(使用main()方法进行测试)的窘境:手工去运行一个个测试用例,这是非常烦琐和低效的,测试套件专门为解决这一问题而来。它通过TestSuite对象将多个测试用例组装成一个测试套件,则测试套件批量运行。需要特别指出的是,可以把一个测试套件整个添加到另一个测试套件中,就像小筐装进大筐里变成一个筐一样。
        JUnit4中最显著的特性是没有套件(套件机制用于将测试从逻辑上分组并将这这些测试作为一个单元测试来运行)。为了替代老版本的套件测试,套件被两个新注解代替:@RunWith、@SuteClasses。通过@RunWith指定一个特殊的运行器,即Suite.class套件运行器,并通过@SuiteClasses注解,将需要进行测试的类列表作为参数传入。

       创建步骤说明如下:

    •   创建一个空类作为测试套件的入口(这个空类必须使用public修饰符,而且存在无参构造函数)。
    •   将@RunWith、@SuiteClasses注释修饰这个空类。
    •   把Suite.class作为参数传入@RunWith注释,以提示JUnit将此类指定为运行器。
    •   将需要测试的类组成数组作为@SuiteClasses的参数。


    断言:Assertions

        断言(assertion)是测试框架里面的若干个方法,用来判断某个语句的结果是否为真或判断是否与预期相符。比如assertTrue这一方法就是用来判定一条语句或一个表达式的结果是否为真,如果条件为假,那么该断言就会执行失败。
        在JUnit 4中一个测试类并不继承自TestCase(在JUnit 3.8中,这个类中定义了assertEquals()方法),所以你必须使用前缀语法(举例来说,Assert.assertEquals())或者静态地导入Assert类。这样我们就可以完全像以前一样使用assertEquals方法。
        由于JDK 5.0自动装箱机制的出现,原先的12个assertEquals方法全部去掉了。例如,原先JUnit 3.8中的assertEquals(long,long)方法在JUnit 4中要使用assertEquals(Object,Object),对于assertEquals(byte,byte)、assertEquals(int,int)等也是这样。
        在JUnit 4中,新集成了一个assert关键字。你可以像使用assertEquals方法一样来使用它,因为它们都抛出相同的异常(java.lang.AssertionError)。JUnit 3.8的assertEquals将抛出一个junit.framework.AssertionFailedError。注意,当使用assert时,你必须指定Java的"-ea"参数,否则断言将被忽略。
    展开全文
  • 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中...
  • 什么单元测试

    千次阅读 2016-06-04 16:39:23
    什么单元测试
  • 经常与单元测试联系起来的另外一些开发活动包括:代码走读、静态分析、动态分析。前面两个不需要对代码进行编译和执行。动态分析就是通过观察软件运行的的动作,来提供执行跟踪,时间分析,以及测试覆盖度方面的信息...
  • iOSUnitTest单元测试

    2021-02-25 16:15:57
    在计算机编程中,单元测试(英语:UnitTesting)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、...
  • 单元测试与集成测试

    万次阅读 多人点赞 2019-09-17 08:25:00
    按测试策略和过程,软件测试分为单元测试、集成测试、确认测试和系统测试。 按软件系统工程,测试是软件质量保证的最后的一关。 高质量的程序取决于以下几个方面: 高质量的设计 规范的编码 有效的测试 开发部...
  • 通常情况下,我们并不会用到它. 创建一个包含单元测试的项目:   Snip20160508_1.png Snip20160508_5.png 单元测试文件夹中包含一个继承自XCTestCase的类.如下:#import<XCTest>@interfaceUnitTestDemoTests:...
  • 单元测试应该测什么

    2015-10-08 20:43:37
    单元测试应该测什么
  • Vue 组件单元测试究竟测试什么

    千次阅读 2020-02-07 15:03:49
    关于 Vue 组件单元测试最常见的问题就是“我究竟应该测试什么?” 虽然测试过多或过少都是可能的,但我的观察是,开发人员通常会测试过头。毕竟,没有人愿意自己的组件未经测试从而导致应用程序在生产中崩溃。 在...
  • IntelliJ IDEA单元测试入门

    万次阅读 多人点赞 2016-08-09 20:10:17
    参考文章地址地址:JUnit4单元测试入门教程 IDEA单元测试及代码覆盖率 IDEA添加jar包的三种方式 本文按以下顺序讲解JUnit4的使用 下载jar包单元测试初体验自动生成测试类执行顺序@Test的属性
  • 前言最近,在网上看到过一个调查,调查的内容是“程序员在项目开发中编写单元测试的情况”。当然,至于调查的结果,我想聪明的你已经可以猜到了。高达 58.3% 的比例,一般情况下不写单元测试,只有偶尔的情况才会写...
  • 软件测试_单元测试和集成测试

    千次阅读 2019-11-25 19:39:27
    什么单元测试 单元测试就是对已实现的软件最小单元进行测试,以保证构成软件的各个单元的质量。 单元测试的目的 单元实现其特定功能 单元的运行能够覆盖预先设定的各种逻辑 在单元工作过程中,其内部数据能够...
  • 单元测试用例

    2013-03-20 15:58:18
    单元测试,junit,包含junit导包,junit单元测试,自动化测试执行,测试技术
  • ios单元测试什么我的应用程序在每次发行后都会变得如此麻烦? 为什么我的质量检查小组经常报告重复性问题和崩溃? 遏制此类问题的最佳解决方案是单元测试。 在此博客文章中,我们将看到如何将单元测试有效地...
  • 一、什么单元测试 测试是软件开发的重要环节之一。按照软件开发的过程测试可分为:单元测试、集成测试、系统测试等。在本文中,我们主要讨论面向程序员的单元测试。 什么单元测试单元测试就是开发者编写的一...
  • 前面向大家介绍了黑盒测试和白盒测试,但是软件测试的方法不单单只有这两种方法,还有单元测试、系统测试、用户验收测试、集成测试、回归测试、冒烟测试、α、β、γ测试、性能测试、安全性测试等。今天要向大家介绍...
  • Javascript单元测试及接口测试

    千次阅读 2018-09-30 16:15:25
    什么单元测试什么要做单元测试 javascript测试框架对比 koa2中如何使用ava做单元测试 vue中如何使用ava做单元测试 koa2中如何使用ava做接口测试 单元测试什么 单元测试(unit testing)指的是以软件的单元...
  • 实用单元测试技术

    2021-03-02 22:58:44
    前言 第一章单元测试的必要性和效益 1.1什么单元测试? 1.2单元测试的必要性 1.3单元测试的效益 第二章代码错误的分类及特点 2.1功能错误与性能问题 2.2代码错误的分类 2.3有特征错误的特点 2.4无特征...
  • 计算机软件单元测试

    2021-02-27 19:08:18
    1主题内容与适用范围1.1主题内容软件单元测试是...1.2适用范围本规范可适用于任何计算机软件的单元测试包括新开发的或修改过的软件单元)。本标准并不规定这些软件的类型,也不规定哪些软件必须进行单元测试。本标准

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 365,398
精华内容 146,159
关键字:

单元测试包括什么