简而言之,JUnit:测试结构

尽管存在关于JUnit测试的书籍和文章,但我仍然经常遇到程序员,他们至多对这个工具及其正确用法都不甚了解。 因此,我想到了编写多部分教程的想法,从我的角度解释了要点。

也许在这个小型系列中采用的动手方法可能适合使一两个额外的开发人员对单元测试感兴趣,这将使工作值得。

上次我介绍了测试的基本知识–测试的编写,执行和评估方式。 在这样做的同时,我概述了测试不仅仅是一个简单的验证机,而且还可以用作一种低级规范。 因此,应该以人们可能想到的最高编码标准来开发它。

这篇文章将继续本教程的示例,并使用Meszaros在xUnit Test Patterns [MES]中定义的命名法,得出表征良好编写的单元测试的通用结构。

测试的四个阶段


整洁的房子,整洁的头脑
老格言

本教程的示例是关于编写一个简单的数字范围计数器,该计数器从给定值开始提供一定数量的连续整数。 从快乐的路径开始,最后一个帖子的结果是一个测试,该测试已验证, NumberRangeCounter在后续调用next方法时返回连续数字:

@Test
  public void subsequentNumber() {    
    NumberRangeCounter counter = new NumberRangeCounter();

    int first = counter.next();
    int second = counter.next();

    assertEquals( first + 1, second );
  }

请注意,本章将坚持使用JUnit内置功能进行验证。 我将在另一篇文章中介绍特定匹配器库( HamcrestAssertJ )的优缺点。

细心的读者可能已经注意到,我使用空行将测试分为不同的部分,并且可能想知道为什么。 为了回答这个问题,让我们更仔细地研究三个部分:

  1. 第一个创建要测试的对象的实例,称为SUT被测系统)。 通常,本节在进行任何与测试相关的活动之前会确定SUT的状态。 由于此状态构成了定义良好的测试输入,因此也称为测试夹具
  2. 建立固定装置后,就该调用SUT的那些方法了, 这些方法代表测试要验证的某种行为。 通常,这只是一个方法,结果存储在局部变量中。
  3. 测试的最后一部分负责验证是否已获得给定行为的预期结果。 尽管有一种思想流传着“每次测试一个声明”的策略,但是我更喜欢“ 每次测试一个概念”的想法,这意味着本节不仅仅局限于一个断言,因为它恰好在示例中[MAR1]。

    这种测试结构非常普遍,并已被多位作者描述。 它被标记为排列,执行,声明 [KAC] –或构建,操作,检查 [MAR2] –模式。 但是,对于本教程,我想精确一点并坚持使用Meszaros的[MES]这四个阶段,分别是 设置(1),练习(2),验证(3)拆卸(4)

  4. 拆卸阶段是为了在长期存在的情况下清理灯具。 持久表示夹具或夹具的一部分将在测试结束后继续存在,并且可能对其后继产品的结果产生不良影响。

普通单元测试很少使用持久性夹具​​,因此拆卸阶段(如我们的示例所示)通常被省略。 而且由于它与规范角度完全不相关,因此无论如何我们都希望将其排除在测试方法之外。 一分钟内将介绍如何实现此目的。

由于这篇文章的范围,我避免了单元测试的精确定义。 但是,我坚持Tomek Kaczanowski使用JUnit和Mockito进行实用单元测试中描述的三种类型的开发人员测试 ,可以概括为:

  • 单元测试可确保您的代码正常运行,并且必须经常运行,因此运行速度非常快。 基本上,这就是本教程的全部内容。
  • 集成测试关注于不同模块的正确集成,包括开发人员无法控制的代码。 这通常需要一些资源(例如数据库,文件系统),因此测试运行速度较慢。
  • 端到端测试从客户端的角度验证您的代码是否有效,并将系统作为一个整体进行测试,以模仿用户的使用方式。 他们通常需要大量时间才能执行自己。
  • 对于如何有效地组合这些测试类型的深入示例,您可以看看Steve FreemanNat Pryce的 Tests指导的Growinging Oriented Oriented Software

但是在继续进行示例之前,还有一个问题需要讨论:

为什么这很重要?


阅读(代码)与写作所花费的时间比例远远超过10:1…
罗伯特·马丁

四个阶段模式的目的是使您易于理解测试正在验证的行为。 安装程序始终定义测试的前提条件,练习实际上会调用测试的行为,验证是否指定了预期的结果,而拆除工作完全与内部维护有关,正如Meszaros所说的那样。

这种干净的相分离清楚地表明了单个测试的意图,并提高了可读性。 该方法意味着测试一次只能验证给定输入状态的一种行为,因此通常没有条件块等(单条件测试)。

试图避免繁琐的夹具安装并在单一方法中测试尽可能多的功能虽然很诱人,但这通常会导致某种性质混淆 。 因此,请始终记住:如果不小心编写测试,可能会给维护和进步带来痛苦。

但是现在是时候继续进行示例了,看看这种新知识可以为我们做什么!

角落案例测试

完成快乐路径测试后,我们将继续指定极端情况行为。 对数字范围计数器的描述指出,数字序列应从给定值开始。 这一点很重要,因为它定义了计数器范围的下限(一个角…)。

将该值作为配置参数传递给NumberRangeCounter的构造函数似乎很合理。 适当的测试可以验证next返回的第一个数字是否等于此初始化:

@Test
  public void lowerBound() {
    NumberRangeCounter counter = new NumberRangeCounter( 1000 );

    int actual = counter.next();
    
    assertEquals( 1000, actual );
  }

再次,我们的测试类不会编译。 通过将lowerBound参数引入计数器的构造函数来解决此问题,则会在subsequentNumber测试中导致编译错误。 幸运的是,后一个测试被编写为独立于下限定义,因此该测试的夹具也可以使用该参数。

但是,测试中的原义数字是多余的,没有明确指出其目的。 后者通常表示为幻数 。 为了改善这种情况,我们可以引入一个常量LOWER_BOUND并替换所有文字值。 以下是测试类的外观:

public class NumberRangeCounterTest {
  
  private static final int LOWER_BOUND = 1000;

  @Test
  public void subsequentNumber() {
    NumberRangeCounter counter = new NumberRangeCounter( LOWER_BOUND );
    
    int first = counter.next();
    int second = counter.next();
    
    assertEquals( first + 1, second );
  }
  
  @Test
  public void lowerBound() {
    NumberRangeCounter counter = new NumberRangeCounter( LOWER_BOUND );

    int actual = counter.next();
    
    assertEquals( LOWER_BOUND, actual );
  }
}

查看代码,您可能会注意到夹具的在线设置对于两种测试都是相同的。 通常,内联设置由多个语句组成,但是测试之间通常存在共同点。 为了避免冗余,可以将共同之处委托给设置方法:

public class NumberRangeCounterTest {
  
  private static final int LOWER_BOUND = 1000;

  @Test
  public void subsequentNumber() {
    NumberRangeCounter counter = setUp();
    
    int first = counter.next();
    int second = counter.next();
    
    assertEquals( first + 1, second );
  }
  
  @Test
  public void lowerBound() {
    NumberRangeCounter counter = setUp();

    int actual = counter.next();
    
    assertEquals( LOWER_BOUND, actual );
  }
  
  private NumberRangeCounter setUp() {
    return new NumberRangeCounter( LOWER_BOUND );
  }
}

如果委托设置方法可以提高给定情况下的可读性,这是有争议的,但它会导致JUnit的一个有趣功能: 隐式执行公共测试设置的可能性。 这可以通过将@Before注释应用于不带返回值和参数的公共非静态方法来实现。

这意味着此功能需要付出一定的代价。 如果要消除测试中的多余setUp调用,则必须引入一个采用NumberRangeCounter实例的字段:

public class NumberRangeCounterTest {
  
  private static final int LOWER_BOUND = 1000;
  
  private NumberRangeCounter counter;
  
  @Before
  public void setUp() {
    counter = new NumberRangeCounter( LOWER_BOUND );
  }

  @Test
  public void subsequentNumber() {
    int first = counter.next();
    int second = counter.next();
    
    assertEquals( first + 1, second );
  }
  
  @Test
  public void lowerBound() {
    int actual = counter.next();
    
    assertEquals( LOWER_BOUND, actual );
  }
}

显而易见, 隐式设置可以消除很多代码重复。 但是从测试的角度来看,它也引入了一种魔术,这会使阅读变得困难。 因此,对于“我应该使用哪种安装类型?”这个问题,答案很明确。 是:这取决于…

由于我通常会注意保持单位/测试较小,因此折衷似乎可以接受。 因此,我经常使用隐式设置来定义公共/快乐路径输入,并为每个极端案例测试通过小的内联/代理设置对它进行相应的补充。 否则,由于特别是初学者倾向于让测试变得更大,因此最好坚持使用内联和委托设置。

JUnit运行时确保在测试类的新实例上调用每个测试。 这意味着在我们的示例中,仅构造函数的灯具可以完全省略setUp方法。 可以通过隐式方式为counter字段分配新的 fixture:

private NumberRangeCounter counter = new NumberRangeCounter( LOWER_BOUND );

尽管有些人@Before使用它,但其他人则认为@Before注释方法会使意图更加明确。 好吧,我不会就此进行战争,让您自己决定的决定……

隐式拆解

想象一下,无论出于何种原因都需要处理NumberRangeCounter 。 这意味着我们必须在测试中添加拆卸阶段。 根据我们的最新代码片段,使用JUnit可以轻松实现,因为它支持使用@After注释进行隐式拆卸 。 我们只需要添加以下方法:

@After
  public void tearDown() {
    counter.dispose();
  }

如上所述,拆卸完全是关于客房清洁的,完全不对特定测试添加任何信息。 因此,隐式执行此操作通常很方便。 或者,即使测试失败,也必须使用try-finally构造来处理此问题,以确保执行拆解。 但是后者通常不会提高可读性。

预期的例外

一个特殊的极端情况是测试预期的异常。 出于示例考虑,如果next的调用超出给定范围的值量,则NumberRangeCalculator应该引发IllegalStateException 。 同样,通过构造函数参数配置范围可能是合理的。 使用try-catch构造,我们可以编写:

@Test
  public void exeedsRange() {
    NumberRangeCounter counter = new NumberRangeCounter( LOWER_BOUND, 0 );

    try {
      counter.next();
      fail();
    } catch( IllegalStateException expected ) {
    }
  }

好吧,这看起来有些丑陋,因为它模糊了测试阶段的分离,并且可读性不强。 但是由于Assert.fail()会引发AssertionError因此可以确保在没有引发异常的情况下测试失败。 并且catch块可以确保在抛出预期异常的情况下成功完成测试。

使用Java 8,可以使用lambda表达式编写结构清晰的异常测试。 有关更多信息,请参阅
使用Java 8 Lambdas清洁JUnit Throwable-Tests

如果足以验证是否已抛出某种类型的异常,则JUnit通过@Test批注的expected方法提供隐式验证 。 上面的测试可以写成:

@Test( expected = IllegalStateException.class )
  public void exeedsRange() {
    new NumberRangeCounter( LOWER_BOUND, ZERO_RANGE ).next();
  }

尽管此方法非常紧凑,但也很危险。 这是因为不能区分是在设置的建立阶段还是在测试的执行阶段抛出了给定的异常。 因此,如果构造函数意外IllegalStateException则测试将是绿色的,因此毫无价值。

JUnit提供了第三种可能性,可以更清晰地测试预期异常,即ExpectedException规则。 由于我们还没有涵盖规则 ,并且该方法有点扭曲了四个阶段的结构,因此我将对该主题的明确讨论推迟到有关规则和运行者的后续文章上并且仅提供摘要作为预告片:

public class NumberRangeCounterTest {
  
  private static final int LOWER_BOUND = 1000; 

  @Rule
  public ExpectedException thrown = ExpectedException.none();

  @Test
  public void exeedsRange() {
    thrown.expect( IllegalStateException.class );
   
    new NumberRangeCounter( LOWER_BOUND, 0 ).next();
  }

  [...]
}

但是,如果您不想等待,可以在RafałBorowiec的JUNIT EXPECTEDEXCEPTION RULE:BEYOND BASICS 》一文中详细了解一下。

结论

简而言之,JUnit的这一章解释了通常用于编写单元测试的四个阶段结构-设置,练习,验证和拆卸。 它描述了每个阶段的目的,并着重强调了在一致使用时如何提高测试用例的可读性。 该示例在极端案例测试的上下文中加深了该学习材料。 希望它具有足够的平衡性,可以提供容易理解的介绍而又不琐碎。 改进建议当然受到高度赞赏。

本教程的下一章将继续该示例,并介绍如何处理单元依赖性和测试隔离,敬请关注。

参考文献

  • [MES] xUnit测试模式,第19章,四阶段测试,Gerard Meszaros,2007年
  • [MAR1]清洁规范,第9章:单元测试,第130页及以下,Robert C. Martin,2009年
  • [KAC]使用JUnit和Mockito进行的实用单元测试,3.9。 单元测试的阶段,Tomek Kaczanowski,2013年
  • [MAR2]清洁代码,第9章:单元测试,第127页,Robert C. Martin,2009年

翻译自: https://www.javacodegeeks.com/2014/08/junit-in-a-nutshell-test-structure.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值