写给精明 Java 开发者的顶级测试技巧

上一篇 / 下一篇  2019-02-11 16:59:25

  我们经常为我们的业务代码写测试用例,对吧?毫无疑问,大多数答案会落在“不错,但是你知道怎样避免它么?”和“当然,我喜欢测试”之间的某种状态。这里我将介绍一些小窍门,让你明白写好测试用例也是如此简单。这也将帮助你写更少的碎片化的测试,以确保你的应用更加强壮。
  同时,如果你的答案是“不,我从来不写测试”,那我也希望这些简单有效的技术让你看到写测试用例的好处,你也将会看到写出明确无价的测试集并不像你想的那样困难。
  如何写测试用例和什么是管理测试套件的最好实践,如今是一个新的主题。
  我们过去已经讨论了很多主题。从如何 在编译流程中正确地使用集成测试 ,到 如何在单元测试中模拟测试环境 ,再到 代码覆盖率和如何找出实际需要测试的代码等 。
  今天,我想给你一些新的思路,教你如何从低级到高级构建测试蓝图,组织测试的心理画像。从如何构造一个简单的单元测试用例,到更高层级的工具的应用等。比如: 你会明白模拟(mock)、侦测(spy)和复制粘贴测试代码(copy-pasting 这里估计是指代码复用)等。让我们开始吧!
  AAArrr, 听起来就像是海盗,对吧~~~
  在大量的软件开发中,找到合适的设计模式来采用会是一个好的开端。你是否想通过工厂创建对象?亦或者是否需要把你的web应用分为模型,视图和控制器等模块?在这背后经常会有一种模式帮助你实现你的想法。那么,一个典型的测试模式应该看起来是什么样的呢?
  在写测试代码时,一个最有效,也最简单的模式是“准备(Arrange)---动作(Act)---断言(Assert)”模型,也叫做 AAA .
  这个模型的前提是:所有的测试应该遵循这个默认布局。被测系统的所有预置条件和输入应该在测试一开始就安排好。等所有前置条件确定后,我们就可以针对被测系统执行 动作(Act) 了, 比如执行一个方法或检查一些系统状态。最后,我们还需要对被测系统产生的结果进行 检查(Assert) 。
  让我们看一个Java JUnit中使用该模式的测试用例:
@Test
public void testAddition() {
// Arrange
Calculator calculator = new Calculator();
// Act
int result = calculator.add(1, 2);
// Assert
assertEquals("Calculator.add returns invalid result", 3, result);
}
  怎么样?这样的代码看起来不错吧? 准备 (Arrange), 动作 (Act), 断言 (Assert)模式可以让你马上明白这个测试用例正在做什么。
  偏离这个模式可能会导致更加凌乱的代码结构。
  请记住迪米特原则
  迪米特原则是指各单元之间应该只使用最少的知识(或联系),以保持松耦合的状态。在软件开发中,迪米特原则总是一个设计目标。
  迪米特原则可以被描述为以下一系列规则:
  在一个方法中,一个类实例可以调用该类中的其它方法。
  在一个方法中,一个实例可以查询它自己的数据,而不能是数据的数据。
  当一个方法需要参数的时候,第一层的方法可以通过给定的参数被调用。
  当一个方法实例化本地变量时,类实例可以调用这些本地变量的方法。
  不要调用全局对象的方法。
  那么,迪米特原则在测试中又意味着什么呢?这意味着你的应用更容易进行单元测试,因为迪米特原则的应用提升了你程序的松耦合度。为了说明该原则如何辅助单元测试,让我们来看看一个不符合该原则的例子:
  考虑以下类,我们需要对它进行测试:
public class Foo() {
public Bar doSomething(Baz aParameter) {
Bar bar = null;
if (aParameter.getValue().isValid()) {
aParameter.getThing().increment();
bar = BarManager.getBar(new Thing());
}
return bar;
}
}
  如果我们尝试测试该方法,因为该类的设计问题,我们将立即遇到一些麻烦。
  在测试该方法的过程中,我们遇到的第一个困难是:我们调用了一个静态方法 --- BarManager.getBar()。该方法在单元测试的约束下是如何工作的?我们没有办法很容易地知道。还记得我们之前讲的"准备,动作,断言“3A模式吗?这里,在调用 doSomething()方法之前(act 动作),我们没有办法对 BarManager 进行配置(Arrange 准备)。如果 BarManager.getBar() 是非静态的,我们可以传递一个 BarManager 实例给 doSomething() 方法,那样也更容易在测试套件中传递统一的用例值,以对该方法的过程进行更好的和可预测的控制。
  在这个方法中,也可以看到我们进行了一个方法链的调用:aParameter.getValue().isValid() 和 aParameter().getThing().increment(). 为了对它们进行测试,我们必须知道对象 aParameter.getValue() 和 aParameter.getThing() 的返回类型是什么?知道了返回类型,我们才可以在测试中构造合适的值对其进行测试。
  如果我们要这样做(译者注:这里指的是构造合适的值),我们必须非常熟悉这些方法返回的对象,并且我们的单元测试将开始变成一大堆不可维护的脆弱代码。我们将打破单元测试的一个基本规则,那就是测试单个单元,而不是这些单元实现的细节。
  我并不是说单元测试只能测试单个类,但是在大多数情况下,将类看作单个单元可能是一个好主意。然而,有时两个或更多的类可以被认为是一个单元。
  我将把它留给读者作为练习,以便将这个方法完全重构为更容易测试的方法。但是对于初学者,我们可以将 aParmater.getValue() 对象作为参数传递到方法中。这将满足我们的一些设定,并使该方法更易于测试。
  知道什么时候才使用断言
  JUnit 和 TesgNG 是两个非常优秀的测试框架,它们提供了丰富的断言方法,比如检查值是否相等或不等,是否为空等。
  是的,我们也认为断言非常酷,那我们就随性地到处使用吧。且慢,且慢,过度使用断言会使得你的测试用例难以维护,我知道那个坑有多深... ...,它会导致应用不可测,不稳定。
  请考虑以下测试方法
@Test
public void testFoo {
// Arrange
Foo foo = new Foo();
double result = …;
// Act
double value = foo.bar( 100.0 );
// Assert
assertEquals(value, result);
assertNotNull( foo.getBar() );
assertTrue( foo.isValid() );
}
  代码乍一看没有问题。我们遵循了AAA模式,也对将要发生的动作进行了正确的断言。这有什么错呢?
  首先,从这个测试方法名字,testFoo,我们无法得到该测试的任何有效信息,它也不能匹配我们正在检查的任何断言。
  那么,如果其中的一个断言失败,我们如何确定被测系统的哪一部分导致的失败呢?是我们的执行动作中 foo.bar(100.0)失败,还是foo.getBar()失败?亦或者是foo.isValid()失败?如果不通过对测试代码的深入debug分析以检查到底发生了什么,那我们就没有办法知道。

TAG: java 测试方法

 

评分:0

我来说两句

日历

« 2024-04-18  
 123456
78910111213
14151617181920
21222324252627
282930    

数据统计

  • 访问量: 17966
  • 日志数: 15
  • 建立时间: 2019-02-03
  • 更新时间: 2019-03-25

RSS订阅

Open Toolbar