Contents

Groovy中Spock测试简介

1. 简介

在本文中,我们将了解Spock ,一个Groovy 测试框架。主要是,Spock 旨在通过利用 Groovy 功能成为传统 JUnit 堆栈的更强大的替代品。

Groovy 是一种与 Java 无缝集成的基于 JVM 的语言。除了互操作性之外,它还提供了额外的语言概念,例如动态、具有可选类型和元编程。

通过使用 Groovy,Spock 引入了新的和富有表现力的方法来测试我们的 Java 应用程序,这在普通的 Java 代码中是不可能的。我们将在本文中探讨 Spock 的一些高级概念,并提供一些实际的逐步示例。

2. Maven依赖

在开始之前,让我们添加我们的Maven 依赖 项:

<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-core</artifactId>
    <version>1.0-groovy-2.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.4.7</version>
    <scope>test</scope>
</dependency>

我们像添加任何标准库一样添加了 Spock 和 Groovy。但是,由于 Groovy 是一种新的 JVM 语言,我们需要包含gmavenplus插件才能编译和运行它:

<plugin>
    <groupId>org.codehaus.gmavenplus</groupId>
    <artifactId>gmavenplus-plugin</artifactId>
    <version>1.5</version>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
                <goal>testCompile</goal>
            </goals>
        </execution>
     </executions>
</plugin>

现在我们准备好编写我们的第一个 Spock 测试,它将用 Groovy 代码编写。请注意,我们仅将 Groovy 和 Spock 用于测试目的,这就是为什么这些依赖项是测试范围的。

3. Spock 测试的结构

3.1. 规格和特点

当我们在 Groovy 中编写测试时,我们需要将它们添加到src/test/groovy目录,而不是src/test/java。让我们在这个目录中创建我们的第一个测试,将其命名为Specification.groovy

class FirstSpecification extends Specification {
}

请注意,我们正在扩展Specification 接口。每个 Spock 类都必须扩展它以使框架对其可用。这样做使我们能够实现我们的第一个feature

def "one plus one should equal two"() {
  expect:
  1 + 1 == 2
}

在解释代码之前,还值得注意的是,在 Spock 中,我们所说的feature 在某种程度上与我们在 JUnit 中看到的test 相同。所以**每当我们提到一个feature时,我们实际上是在指一个*test *

现在,让我们分析一下我们的feature。这样做,我们应该立即能够看到它与 Java 之间的一些差异。

第一个区别是特征方法名写成普通字符串。在 JUnit 中,我们将有一个使用驼峰或下划线分隔单词的方法名称,这样就不会具有表达力或人类可读性。

接下来是我们的测试代码存在于一个*expect *块中。稍后我们将更详细地介绍块,但本质上它们是划分测试不同步骤的合乎逻辑的方式。

最后,我们意识到没有断言。这是因为断言是隐式的,当我们的语句等于true时通过,当它等于false时失败。同样,我们很快将更详细地介绍断言。

3.2. 块

有时在编写 JUnit 测试时,我们可能会注意到没有一种表达方式可以将其分解为多个部分。例如,如果我们遵循行为驱动的开发,我们最终可能会使用注释来表示given when then部分:

@Test
public void givenTwoAndTwo_whenAdding_thenResultIsFour() {
   // Given
   int first = 2;
   int second = 4;
   // When
   int result = 2 + 2;
   // Then
   assertTrue(result == 4)
}

Spock 用块解决了这个问题。**块是 Spock 使用标签分解测试阶段的原生方式。**他们给我们的标签是given when then和更多:

  1. *Setup *(由 Given 起别名)– 在这里,我们在运行测试之前执行所需的任何设置。这是一个隐式块,代码根本不在任何块中成为它的一部分
  2. *When ——这是我们为正在测试的内容提供stimulus *。换句话说,我们在哪里调用我们的测试方法
  3. *Then *——这就是断言所属的地方。在 Spock 中,这些被评估为普通的布尔断言,稍后将介绍
  4. *Expect ——这是在同一块内执行我们的stimulus assertion *的一种方式。根据我们发现更具表现力的内容,我们可能会或可能不会选择使用此块
  5. *Cleanup *- 在这里,我们拆除任何否则会留下的测试依赖资源。例如,我们可能想从文件系统中删除任何文件或删除写入数据库的测试数据

让我们再次尝试实现我们的测试,这次充分利用块:

def "two plus two should equal four"() {
    given:
        int left = 2
        int right = 2
    when:
        int result = left + right
    then:
        result == 4
}

正如我们所见,块有助于我们的测试变得更具可读性。

3.3. 利用 Groovy 特性进行断言

thenexpect块中,断言是隐式的

大多数情况下,每个语句都会被评估,如果它不是true则失败。当将此与各种 Groovy 特性结合起来时,它可以很好地消除对断言库的需求。让我们尝试一个list 断言来证明这一点:

def "Should be able to remove from list"() {
    given:
        def list = [1, 2, 3, 4]
    when:
        list.remove(0)
    then:
        list == [2, 3, 4]
}

虽然我们在本文中只是简要介绍了 Groovy,但有必要解释一下这里发生的事情。

首先,Groovy 为我们提供了更简单的创建列表的方法。我们可以用方括号声明我们的元素,并且在内部将实例化一个list

其次,由于 Groovy 是动态的,我们可以使用def,这意味着我们没有为变量声明类型。

最后,在简化我们的测试的背景下,展示的最有用的特性是运算符重载。这意味着在内部,而不是像在 Java 中那样进行引用比较,将调用*equals()*方法来比较两个列表。

还值得展示当我们的测试失败时会发生什么。让我们让它中断,然后查看控制台的输出:

Condition not satisfied:
list == [1, 3, 4]
|    |
|    false
[2, 3, 4]
 <Click to see difference>
at FirstSpecification.Should be able to remove from list(FirstSpecification.groovy:30)

虽然正在发生的只是在两个列表上调用equals(),但 Spock 足够聪明,可以对失败的断言进行分解,为我们提供有用的调试信息。

3.4. 断言异常

Spock 还为我们提供了一种检查异常的表达方式。在 JUnit 中,我们的一些选项可能是使用try-catch块,在我们的测试顶部声明*expected *,或者使用第三方库。Spock 的原生断言提供了一种开箱即用的异常处理方式:

def "Should get an index out of bounds when removing a non-existent item"() {
    given:
        def list = [1, 2, 3, 4]
    when:
        list.remove(20)
    then:
        thrown(IndexOutOfBoundsException)
        list.size() == 4
}

在这里,我们不必引入额外的库。另一个优点是*throws()*方法将断言异常的类型,但不会停止测试的执行。

4. 数据驱动测试

4.1. 什么是数据驱动测试?

本质上,数据驱动测试是我们使用不同的参数和断言多次测试相同的行为。一个典型的例子是测试数学运算,例如平方数。根据操作数的各种排列,结果会有所不同。在 Java 中,我们可能更熟悉的术语是参数化测试。

4.2. 在 Java 中实现参数化测试

在某些情况下,值得使用 JUnit 实现参数化测试:

@RunWith(Parameterized.class)
public class FibonacciTest {
    @Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] {
          { 1, 1 }, { 2, 4 }, { 3, 9 }  
        });
    }
    private int input;
    private int expected;
    public FibonacciTest (int input, int expected) {
        this.input = input;
        this.expected = expected;
    }
    @Test
    public void test() {
        assertEquals(fExpected, Math.pow(3, 2));
    }
}

正如我们所看到的那样,有很多冗长的代码,而且代码的可读性也不是很好。我们必须创建一个位于测试之外的二维对象数组,甚至是一个用于注入各种测试值的包装器对象。

4.3. 在 Spock 中使用数据表

与 JUnit 相比,Spock 的一个轻松胜利是它如何干净地实现参数化测试。同样,在 Spock 中,这被称为数据驱动测试。现在,让我们再次实现相同的测试,只是这次我们将使用 Spock 和Data Tables,它提供了一种更方便的方式来执行参数化测试:

def "numbers to the power of two"(int a, int b, int c) {
  expect:
      Math.pow(a, b) == c
  where:
      a | b | c
      1 | 2 | 1
      2 | 2 | 4
      3 | 2 | 9
  }

正如我们所看到的,我们只有一个简单且富有表现力的数据表,其中包含我们所有的参数。

此外,它属于它应该做的地方,与测试一起,并且没有样板。该测试是富有表现力的,具有人类可读的名称,以及纯粹的expectwhere块来分解逻辑部分。

4.4. 当数据表失败时

还值得看看当我们的测试失败时会发生什么:

Condition not satisfied:
Math.pow(a, b) == c
     |   |  |  |  |
     4.0 2  2  |  1
               false
Expected :1
Actual   :4.0

再一次,Spock 给了我们一个非常有用的错误信息。我们可以准确地看到 Datatable 的哪一行导致失败以及原因。

5. 模拟

5.1. 什么是模拟?

模拟是一种改变我们的被测服务与之协作的类的行为的方法。这是一种能够隔离其依赖项来测试业务逻辑的有用方法。 一个典型的例子是用简单的假装来替换一个进行网络调用的类。更深入的解释,这篇文章 值得一读。

5.2. 使用 Spock 进行模拟

Spock 有自己的模拟框架,利用了 Groovy 为 JVM 带来的有趣概念。首先,让我们实例化一个Mock

PaymentGateway paymentGateway = Mock()

在这种情况下,我们的模拟类型由变量类型推断。由于 Groovy 是一种动态语言,我们还可以提供类型参数,让我们不必将模拟分配给任何特定类型:

def paymentGateway = Mock(PaymentGateway)

现在,每当我们调用PaymentGateway mock上的方法时,都会给出默认响应,而不会调用真实实例:

when:
    def result = paymentGateway.makePayment(12.99)
then:
    result == false

对此的说法是lenient mocking。这意味着尚未定义的模拟方法将返回合理的默认值,而不是抛出异常。这是在 Spock 中设计的,目的是为了让 mocks 变得不那么脆弱。

5.3. Mocks上的存根方法调用

我们还可以配置在我们的模拟上调用的方法以某种方式响应不同的参数。让我们尝试在支付 20 时让PaymentGateway模拟返回true

given:
    paymentGateway.makePayment(20) >> true
when:
    def result = paymentGateway.makePayment(20)
then:
    result == true

这里有趣的是,Spock 如何利用 Groovy 的运算符重载来存根方法调用。使用 Java,我们必须调用真正的方法,这可以说意味着生成的代码更冗长,并且可能表达能力较差。

现在,让我们尝试更多类型的存根。

如果我们不再关心我们的方法参数并且总是想返回true,我们可以只使用下划线:

paymentGateway.makePayment(_) >> true

如果我们想在不同的响应之间交替,我们可以提供一个列表,每个元素将按顺序返回:

paymentGateway.makePayment(_) >>> [true, true, false, true]

还有更多的可能性,这些可能会在未来更高级的关于模拟的文章中介绍。

5.4. 确认

我们可能想要对 mock 做的另一件事是断言使用预期参数调用了各种方法。换句话说,我们应该验证与我们的模拟的交互。

一个典型的验证用例是,如果我们的 mock 上的方法具有void返回类型。在这种情况下,由于没有结果可供我们操作,因此我们无法通过被测方法进行测试的推断行为。一般来说,如果有东西被返回,那么被测试的方法就可以对其进行操作,并且该操作的结果就是我们所断言的。

让我们尝试验证一个返回类型为 void 的方法是否被调用:

def "Should verify notify was called"() {
    given:
        def notifier = Mock(Notifier)
    when:
        notifier.notify('foo')
    then:
        1 * notifier.notify('foo')
}

Spock 再次利用 Groovy 运算符重载。通过将我们的 mocks 方法调用乘以 1,我们表示我们期望它被调用了多少次。

如果我们的方法根本没有被调用,或者没有像我们指定的那样被调用多次,那么我们的测试将无法给我们提供信息丰富的 Spock 错误消息。让我们通过期望它被调用两次来证明这一点:

2 * notifier.notify('foo')

接下来,让我们看看错误消息的样子。我们会像往常一样;它的信息量很大:

Too few invocations for:
2 * notifier.notify('foo')   (1 invocation)

就像存根一样,我们也可以执行更松散的验证匹配。如果我们不关心方法参数是什么,我们可以使用下划线:

2 * notifier.notify(_)

或者,如果我们想确保它没有被特定参数调用,我们可以使用 not 运算符:

2 * notifier.notify(!'foo')

同样,还有更多的可能性,可能会在未来更高级的文章中介绍。