使用 JUnit4编写单元测试

原文连接: blog.yoryor.top/2017-11-02/…html

  • 主要内容: 本文从 What, Why, When, How, Deep 几个方面来介绍单元测试相关的基础知识。
  • 适合阅读: 对单元测试不了解或者只知其一;不知其二的程序员
  • 须要技能: 了解 Java 编程语言,可以使用 IDEA 等。

What

单元测试(unit test)并不一样于普通的端到端测试,这是一项须要程序员经过实际编码来完成,而且与程序的设计和调试紧密相关的任务。单元的大小,并无严格规定,可是遵守着软件设计中”SRP”(Single Responsibility Principle)原则,在 Java 中一般以类做为一个基本的测试单元,以此编写单元测试来对功能或者说行为进行监视和检测。
JUnit 是 Java 编程语言中比较流行的一款单元测试框架,可以知足平时开发中大部分的需求。java

Why

编写单元测试的主要目的是为了检视代码的行为是否符合预期,而且这种检查一般是成本很低的,开发人员能够方便的在 IDE 或本地开发环境中及时的发现问题,从而提升开发效率。git

When

编写合理的单元测试能够轻松应付如下的几个场景程序员

  • 检查新添加的代码功能是否符合预期
  • 检查新添加的代码是否兼容旧版本
  • 第三方库代码的运行是否符合预期
  • 了解当前代码的行为

How

在 IDEA 中使用单元测试是很方便的,能够参考官方的教程建立单元测试
接下来使用的示例代码模拟实现了一个功能,根据一个程序员的技能和等级,为一个程序员打分。详细的代码能够到 github 上查看 code-example, 推荐根据提交历史来进行查看。具体的环境以下:github

  • JDK:1.8+
  • IDEA:2017.2
  • JUnit: 4.12

注解

Junit4 开始使用的注释提升了单元测试的编写效率,在引入 Junit4依赖后,在须要进行测试的方法上添加一个@Test 注释便可。其余经常使用的注释还有@Before、@After、@Rule 等。在后面遇到的时候在详述编程

命名

良好的测试方法命名能起到见名知义的效果。理想状况下,命名中须要包含测试的方法,条件以及指望的返回结果。能够提炼成如下的格式
·callSomeMethodReturnSomeResultWhenSomeConditions
或者其余的相似的变体。好比given-then-when等等框架

方法体

前面的命名中提到的三个信息也正是组织单元测试的基本依据。即编程语言

  1. 准备条件 (arrange)
  2. 执行目标方法 (act)
  3. 检验结果 (assert)
    Assert 这个单词的直译是断言,意思是判断某项条件是否为真。在单元测试中咱们经过断言来实现结果检验的语义, 以此来监视代码的行为。
    JUnit 中可使用两种不一样的断言风格— classic 和 matcher。经过代码能够直观的看出两者之间的差异。ide

    public class SkillGraphTest {
    
         @Test
         public void getResultReturnZeroWhenSkillGraphEmpty() throws Exception {
             // arrange
             SkillGraph skills = new SkillGraph();
             // act and assert -- classic style
             assertTrue(skills.getResult() == 0);
             // act and assert -- matchers style
             assertThat(skills.getResult(), equalTo(0d));
         }
     }复制代码

    上面为 SkillGraph 类编写的单元测试示例中,咱们验证的行为是当程序员的技能图中没有添加任何技能时调用 getResult 方法须要返回0(double 类型)
    因为代码很简单,因此将执行和验证的两个阶段合并到一块儿了。单元测试

咱们主要看一下不一样风格的断言:

  1. classic 风格,即验证某项条件为真。在这个方法中即须要验证执行方法的结果 skills.getResult与咱们指望的结果是否相等。也就是skills.getResult() == 0
  2. matchers 风格,经过语义化的代码来验证结果是否匹配。整个代码不须要太多的逻辑思考,更贴近咱们的阅读习惯 — skills.getResult() is equal to 0d
    在 Junit 中,Assert 类中提供了基础的断言 API。经过 assertThat(actualResult, matchers) 方法能够利用 Hamcrest 开发的大量 Matcher 为咱们的单元测试提供更好的语义化支持。
    在此,我不强烈推荐其中任何一种,可是须要注意的是一个项目中的断言风格应该尽可能保持一直。

测试异常

在编写单元测试时,若是测试结果不符合预期,JUnit 会报告一次failure;若是单元测试程序运行过程当中若是出现了异常,则会报告一次error。因为单元测试的隔离性,咱们一般将关注点集中在须要测试的功能上,而不须要浪费时间在异常处理上,所以一些受检查异常直接在方法签名上抛出便可。
有些时候咱们可能会须要验证异常抛出的正确性,以此来保证客户端使用的正确性和可靠性,有三种形式能够来验证异常行为是否合理。

  • 在 @Test 注解中增长 expected 属性。以下代码若是去掉 expected 属性,执行单元测试则会因为运行时异常而报告一次 error
    @Test(expected = IllegalArgumentException.class)
          public void getResultThrowIllegalArgumentExceptionWhenAddNullValue() throws Exception {
              // arrange
              SkillGraph skills = new SkillGraph();
              // act
              skills.add(null);
              // assert
          }复制代码
  • 使用 try-catch 方法来验证。以下,虽然能够实现,可是不够优雅。
    @Test
          public void getResultThrowIllegalArgumentExceptionWhenAddNullValue() throws Exception {
              // arrange
              SkillGraph skills = new SkillGraph();
              // act
              try {
                  skills.add(null);
              } catch (Exception e) {
          // assert
                  assertThat(e, instanceOf(IllegalArgumentException.class));
                  assertThat(e.getMessage(), equalTo("Skill can not be null."));
              }
          }复制代码
  • 使用 @Rule 注解,利用内置的 ExpectedException 来实现。JUnit 中的 rule 规则机制实际上就是相似一种 AOP 的实现,为单元测试提供面向切面编程的能力,详细的内容再也不此展开了。异常的断言要放在前面,不然代码就没法执行到

    @Rule
          public ExpectedException expectedException = ExpectedException.none();
    
          @Test
          public void getResultThrowIllegalArgumentExceptionWhenAddNullValue() throws Exception {
              expectedException.expectMessage("Skill can not be null.");
              expectedException.expect(IllegalArgumentException.class);
              // arrange
              SkillGraph skills = new SkillGraph();
              // act
              skills.add(null);
              // assert
          }复制代码

Deep

优化重构测试代码

在编写单元测试时,分支条件边界条件是须要重点关注的。这也会致使一个类的单元测试每每须要编写不少单元测试。所以有必要经过优化重构来保持代码的整洁和良好的可维护性和扩展性。
在单元测试中,咱们在 arrange 阶段每每会执行一些对象初始化等一些初始化操做,这个过程一般是重复的。能够经过@Before 注解一个初始化方法,这样在每一个单元测试以前都会执行这段代码了。相似的还有@After注解,只不过不太经常使用。
其实以前提到的@Rule注解实现的就是相似@Before@After的功能,在执行一个单元测试方法的先后执行一些方法,只不过这些方法都是有具体的目标,经过规则这个语义实现。

注: JUnit 中每一个单元测试都拥有独立的上下文环境,执行每一个测试方法时都会单独生成一个新的实例,所以没法保证单元测试用例的执行顺序,致使咱们在单元测试中不能依赖其余的测试结果,固然这种需求自己就是“anti pattern”的。

JUnit 运行

一个典型的单元测试用例的执行以下

  1. 建立单元测试类实例
  2. 调用@Before 方法
  3. 执行某一个@Test注释的方法
  4. 调用@After 方法
  5. 建立新的单元测试类实例
  6. 调用@Before 方法
  7. 执行另外某一个@Test注释的方法
  8. 调用@After 方法
    … …

边界条件

编写单元测试最主要、最直接的关注点就是方法执行的正确性。其次须要关注数据的边界条件,即在某些极端的或者不正确的条件下,程序可否正常的运行或者合理的运行,以此来保证系统的健壮性。常见的关注点有如下几个

  1. 不合实际的值(例:表示人类年龄的字段输入180或者负数)
  2. 不符合格式的值 (例:邮件,手机等)
  3. 算数溢出
  4. 空值,Null,空集合等
  5. 不合理的重复值
  6. 没有合理排序的值
  7. 事件发生的顺序异常(例: 在建立用户以前就进行信息编辑等)

小结

编写单元测试算是一项内功修炼,也是一项烦琐的工做。曾国藩曾说过,“成大事者,必能耐烦”。若是单纯为了提升代码覆盖率,确实很烦;若是为了改进代码结构防患 Bug 于未然,就会发现单元测试是如此有用。

相关文章
相关标签/搜索