Introduction to Testing with Spock and Groovy – 用Spock和Groovy进行测试的介绍

最后修改: 2017年 3月 23日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

In this article, we’ll take a look at Spock, a Groovy testing framework. Mainly, Spock aims to be a more powerful alternative to the traditional JUnit stack, by leveraging Groovy features.

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

Groovy is a JVM-based language which seamlessly integrates with Java. On top of interoperability, it offers additional language concepts such as being a dynamic, having optional types and meta-programming.

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

By making use of Groovy, Spock introduces new and expressive ways of testing our Java applications, which simply aren’t possible in ordinary Java code. We’ll explore some of Spock’s high-level concepts during this article, with some practical step by step examples.

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

2. Maven Dependency

2.Maven的依赖性

Before we get started, let’s add our Maven dependencies:

在开始之前,我们先添加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>

We’ve added both Spock and Groovy as we would any standard library. However, as Groovy is a new JVM language, we need to include the gmavenplus plugin in order to be able to compile and run it:

我们像对待任何标准库一样,同时添加了Spock和Groovy。然而,由于Groovy是一种新的JVM语言,我们需要加入gmavenplus plugin,以便能够编译和运行它。

<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>

Now we are ready to write our first Spock test, which will be written in Groovy code. Note that we are using Groovy and Spock only for testing purposes and this is why those dependencies are test-scoped.

现在我们准备编写我们的第一个Spock测试,它将用Groovy代码编写。注意,我们使用Groovy和Spock只是为了测试,这就是为什么这些依赖关系是测试范围的。

3. Structure of a Spock Test

3.Spock测试的结构

3.1. Specifications and Features

3.1.规格和特点

As we are writing our tests in Groovy, we need to add them to the src/test/groovy directory, instead of src/test/java. Let’s create our first test in this directory, naming it Specification.groovy:

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

class FirstSpecification extends Specification {

}

Note that we are extending the Specification interface. Each Spock class must extend this in order to make the framework available to it. It’s doing so that allows us to implement our first feature:

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

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

Before explaining the code, it’s also worth noting that in Spock, what we refer to as a feature is somewhat synonymous to what we see as a test in JUnit. So whenever we refer to a feature we are actually referring to a test.

在解释代码之前,还值得注意的是,在Spock中,我们所说的feature与我们在JUnit中看到的test是同义的。因此,当我们提到一个特征时,我们实际上是指一个测试

Now, let’s analyze our feature. In doing so, we should immediately be able to see some differences between it and Java.

现在,让我们分析一下我们的特征。在这样做的过程中,我们应该立即能够看到它与Java的一些区别。

The first difference is that the feature method name is written as an ordinary string. In JUnit, we would have had a method name which uses camelcase or underscores to separate the words, which would not have been as expressive or human readable.

第一个区别是,特征方法名被写成了一个普通的字符串。在JUnit中,我们会有一个使用驼峰大写或下划线来分隔单词的方法名,这样的方法名就没有那么强的表现力和可读性。

The next is that our test code lives in an expect block. We will cover blocks in more detail shortly, but essentially they are a logical way of splitting up the different steps of our tests.

其次是我们的测试代码生活在一个expect 块中。我们将在短期内更详细地介绍块,但本质上它们是分割测试不同步骤的一种逻辑方式。

Finally, we realize that there are no assertions. That’s because the assertion is implicit, passing when our statement equals true and failing when it equals false. Again, we’ll cover assertions in more details shortly.

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

3.2. Blocks

3.2.块状物

Sometimes when writing JUnit a test, we might notice there isn’t an expressive way of breaking it up into parts. For example, if we were following behavior driven development, we might end up denoting the given when then parts using comments:

有时在编写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 addresses this problem with blocks. Blocks are a Spock native way of breaking up the phases of our test using labels. They give us labels for given when then and more:

Spock用块来解决这个问题。块是Spock的一种原生方式,它使用标签来分割我们测试的各个阶段。它们为given when then等提供了标签。

  1. Setup (Aliased by Given) – Here we perform any setup needed before a test is run. This is an implicit block, with code not in any block at all becoming part of it
  2. When – This is where we provide a stimulus to what is under test. In other words, where we invoke our method under test
  3. Then – This is where the assertions belong. In Spock, these are evaluated as plain boolean assertions, which will be covered later
  4. Expect – This is a way of performing our stimulus and assertion within the same block. Depending on what we find more expressive, we may or may not choose to use this block
  5. Cleanup – Here we tear down any test dependency resources which would otherwise be left behind. For example, we might want to remove any files from the file system or remove test data written to a database

Let’s try implementing our test again, this time making full use of blocks:

让我们再次尝试实现我们的测试,这次要充分使用块。

def "two plus two should equal four"() {
    given:
        int left = 2
        int right = 2

    when:
        int result = left + right

    then:
        result == 4
}

As we can see, blocks help our test become more readable.

正如我们所看到的,区块帮助我们的测试变得更加可读。

3.3. Leveraging Groovy Features for Assertions

3.3.充分利用Groovy的断言功能

Within the then and expect blocks, assertions are implicit.

thenexpect块中,断言是隐含的

Mostly, every statement is evaluated and then fails if it is not true. When coupling this with various Groovy features, it does a good job of removing the need for an assertion library. Let’s try a list assertion to demonstrate this:

大多数情况下,每个语句都会被评估,如果不是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]
}

While we’re only touching briefly on Groovy in this article, it’s worth explaining what is happening here.

虽然我们在这篇文章中只是简单地触及了Groovy,但还是值得解释一下这里发生了什么。

First, Groovy gives us simpler ways of creating lists. We can just able to declare our elements with square brackets, and internally a list will be instantiated.

首先,Groovy给了我们创建列表的更简单的方法。我们只需用方括号声明我们的元素,在内部就会有一个list被实例化。

Secondly, as Groovy is dynamic, we can use def which just means we aren’t declaring a type for our variables.

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

Finally, in the context of simplifying our test, the most useful feature demonstrated is operator overloading. This means that internally, rather than making a reference comparison like in Java, the equals() method will be invoked to compare the two lists.

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

It’s also worth demonstrating what happens when our test fails. Let’s make it break and then view what’s output to the console:

也值得展示一下当我们的测试失败时会发生什么。让我们把它弄坏,然后查看输出到控制台的内容。

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)

While all that’s going on is calling equals() on two lists, Spock is intelligent enough to perform a breakdown of the failing assertion, giving us useful information for debugging.

当所有这些都是在两个列表上调用equals()时,Spock足够聪明,对失败的断言进行了分解,为我们提供了有用的调试信息。

3.4. Asserting Exceptions

3.4.断言异常

Spock also provides us with an expressive way of checking for exceptions. In JUnit, some our options might be using a try-catch block, declare expected at the top of our test, or making use of a third party library. Spock’s native assertions come with a way of dealing with exceptions out of the box:

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
}

Here, we’ve not had to introduce an additional library. Another advantage is that the thrown() method will assert the type of the exception, but not halt execution of the test.

在这里,我们没有必要引入一个额外的库。另一个好处是,thrown() 方法将断定异常的类型,但不会停止测试的执行。

4. Data Driven Testing

4.数据驱动的测试

4.1. What Is a Data Driven Testing?

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

Essentially, data driven testing is when we test the same behavior multiple times with different parameters and assertions. A classic example of this would be testing a mathematical operation such as squaring a number. Depending on the various permutations of operands, the result will be different. In Java, the term we may be more familiar with is parameterized testing.

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

4.2. Implementing a Parameterized Test in Java

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

For some context, it’s worth implementing a parameterized test using JUnit:

为了了解一些情况,值得用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));
    }
}

As we can see there’s quite a lot of verbosity, and the code isn’t very readable. We’ve had to create a two-dimensional object array that lives outside of the test, and even a wrapper object for injecting the various test values.

正如我们所看到的,有相当多的言辞,而且代码不是很好读。我们不得不创建一个生活在测试之外的二维对象数组,甚至还有一个用于注入各种测试值的封装对象。

4.3. Using Datatables in Spock

4.3.在Spock中使用数据表格

One easy win for Spock when compared to JUnit is how it cleanly it implements parameterized tests. Again, in Spock, this is known as Data Driven Testing. Now, let’s implement the same test again, only this time we’ll use Spock with Data Tables, which provides a far more convenient way of performing a parameterized test:

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

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
  }

As we can see, we just have a straightforward and expressive Data table containing all our parameters.

正如我们所看到的,我们只是有一个简单明了、富有表现力的数据表,包含了我们所有的参数。

Also, it belongs where it should do, alongside the test, and there is no boilerplate. The test is expressive, with a human-readable name, and pure expect and where block to break up the logical sections.

而且,它属于它应该做的地方,与测试一起,而且没有模板。测试是有表现力的,有一个人类可读的名字,以及纯粹的expect where 块来分割逻辑部分。

4.4. When a Datatable Fails

4.4.当可数据化对象失败时

It’s also worth seeing what happens when our test fails:

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

Condition not satisfied:

Math.pow(a, b) == c
     |   |  |  |  |
     4.0 2  2  |  1
               false

Expected :1

Actual   :4.0

Again, Spock gives us a very informative error message. We can see exactly what row of our Datatable caused a failure and why.

同样,Spock给了我们一个非常有信息量的错误信息。我们可以准确地看到我们的Datatable的哪一行导致了失败,以及为什么。

5. Mocking

5.嘲笑

5.1. What Is Mocking?

5.1.什么是嘲弄?

Mocking is a way of changing the behavior of a class which our service under test collaborates with. It’s a helpful way of being able to test business logic in isolation of its dependencies.

嘲弄是一种改变我们被测试的服务与之合作的类的行为的方式。这是一种有用的方法,能够测试业务逻辑与它的依赖关系相隔离。

A classic example of this would be replacing a class which makes a network call with something which simply pretends to. For a more in-depth explanation, it’s worth reading this article.

这方面的一个典型例子是将一个进行网络调用的类替换成一个简单的假装。对于更深入的解释,值得阅读这篇文章

5.2. Mocking Using Spock

5.2.使用Spock进行调试

Spock has it’s own mocking framework, making use of interesting concepts brought to the JVM by Groovy. First, let’s instantiate a Mock:

Spock有自己的嘲讽框架,利用了Groovy带给JVM的有趣概念。首先,让我们实例化一个Mock:

PaymentGateway paymentGateway = Mock()

In this case, the type of our mock is inferred by the variable type. As Groovy is a dynamic language, we can also provide a type argument, allow us to not have to assign our mock to any particular type:

在这种情况下,我们的mock的类型是由变量类型推断出来的。由于Groovy是一种动态语言,我们也可以提供一个类型参数,让我们不必把我们的mock分配给任何特定的类型。

def paymentGateway = Mock(PaymentGateway)

Now, whenever we call a method on our PaymentGateway mock, a default response will be given, without a real instance being invoked:

现在,每当我们在我们的PaymentGatewaymock上调用一个方法时,将给出一个默认的响应,而没有调用一个真正的实例。

when:
    def result = paymentGateway.makePayment(12.99)

then:
    result == false

The term for this is lenient mocking. This means that mock methods which have not been defined will return sensible defaults, as opposed to throwing an exception. This is by design in Spock, in order to make mocks and thus tests less brittle.

这方面的术语是lenient mocking。这意味着尚未定义的mock方法将返回合理的默认值,而不是抛出一个异常。这是Spock的设计,目的是为了使模拟和测试不那么脆。

5.3. Stubbing Method Calls on Mocks

5.3.在Mocks上存根方法调用

We can also configure methods called on our mock to respond in a certain way to different arguments. Let’s try getting our PaymentGateway mock to return true when we make a payment of 20:

我们还可以配置在我们的模拟上调用的方法,以便对不同的参数作出某种回应。让我们试着让我们的PaymentGatewaymock在我们支付20的时候返回true

given:
    paymentGateway.makePayment(20) >> true

when:
    def result = paymentGateway.makePayment(20)

then:
    result == true

What’s interesting here, is how Spock makes use of Groovy’s operator overloading in order to stub method calls. With Java, we have to call real methods, which arguably means that the resulting code is more verbose and potentially less expressive.

这里有趣的是,Spock是如何利用Groovy的操作符重载来存根方法调用的。在Java中,我们必须调用真正的方法,这可以说意味着所产生的代码更加冗长,而且可能缺乏表达能力。

Now, let’s try a few more types of stubbing.

现在,让我们再试一下几种类型的存根。

If we stopped caring about our method argument and always wanted to return true, we could just use an underscore:

如果我们不再关心我们的方法参数,而总是想返回true,我们可以直接使用下划线。

paymentGateway.makePayment(_) >> true

If we wanted to alternate between different responses, we could provide a list, for which each element will be returned in sequence:

如果我们想在不同的响应之间交替进行,我们可以提供一个列表,其中每个元素将被依次返回。

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

There are more possibilities, and these may be covered in a more advanced future article on mocking.

还有更多的可能性,这些可能会在未来关于嘲弄的更高级的文章中涉及。

5.4. Verification

5.4.验证

Another thing we might want to do with mocks is assert that various methods were called on them with expected parameters. In other words, we ought to verify interactions with our mocks.

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

A typical use case for verification would be if a method on our mock had a void return type. In this case, by there being no result for us to operate on, there’s no inferred behavior for us to test via the method under test. Generally, if something was returned, then the method under test could operate on it, and it’s the result of that operation would be what we assert.

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

Let’s try verifying that a method with a void return type is called:

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

def "Should verify notify was called"() {
    given:
        def notifier = Mock(Notifier)

    when:
        notifier.notify('foo')

    then:
        1 * notifier.notify('foo')
}

Spock is leveraging Groovy operator overloading again. By multiplying our mocks method call by one, we are saying how many times we expect it to have been called.

Spock又在利用Groovy的操作符重载了。通过将我们的mocks方法调用乘以1,我们就可以说我们希望它被调用了多少次。

If our method had not been called at all or alternatively had not been called as many times as we specified, then our test would have failed to give us an informative Spock error message. Let’s prove this by expecting it to have been called twice:

如果我们的方法根本没有被调用,或者没有按照我们指定的次数被调用,那么我们的测试就会失败,不能给我们提供一个信息丰富的Spock错误信息。让我们通过期望它被调用两次来证明这一点。

2 * notifier.notify('foo')

Following this, let’s see what the error message looks like. We’ll that as usual; it’s quite informative:

在这之后,让我们看看错误信息是什么样子的。我们会像往常一样;它的信息量很大。

Too few invocations for:

2 * notifier.notify('foo')   (1 invocation)

Just like stubbing, we can also perform looser verification matching. If we didn’t care what our method parameter was, we could use an underscore:

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

2 * notifier.notify(_)

Or if we wanted to make sure it wasn’t called with a particular argument, we could use the not operator:

或者,如果我们想确保它没有被调用的特定参数,我们可以使用not操作符。

2 * notifier.notify(!'foo')

Again, there are more possibilities, which may be covered in a future more advanced article.

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

6. Conclusion

6.结论

In this article, we’ve given a quick slice through testing with Spock.

在这篇文章中,我们已经给出了一个用Spock进行测试的快速片断。

We’ve demonstrated how, by leveraging Groovy, we can make our tests more expressive than the typical JUnit stack. We’ve explained the structure of specifications and features.

我们已经展示了如何通过利用Groovy,使我们的测试比典型的JUnit堆栈更具表现力。我们解释了specificationsfeatures的结构。

And we’ve shown how easy it is to perform data-driven testing, and also how mocking and assertions are easy via native Spock functionality.

我们已经展示了执行数据驱动的测试是多么容易,以及通过原生的Spock功能,嘲弄和断言是多么容易。

The implementation of these examples can be found over on GitHub. This is a Maven-based project, so should be easy to run as is.

这些例子的实现可以在GitHub上找到over。这是一个基于Maven的项目,所以应该很容易按原样运行。