1. Introduction
1.导言
Spock is a great framework for writing tests, especially regarding increasing test coverage.
Spock 是编写测试的绝佳框架,尤其是在提高测试覆盖率方面。
In this tutorial, we’ll explore Spock’s data pipes and how to improve our line and branch code coverage by adding extra data to a data pipe. We’ll also look at what to do when our data gets too big.
在本教程中,我们将探讨 Spock 的数据管道,以及如何通过向数据管道添加额外数据来提高行和分支代码覆盖率。我们还将研究当数据过大时该怎么办。
2. The Subject of Our Test
2.我们的测试对象
Let’s start with a method that adds two numbers but with a twist. If the first or second number is 42, then return 42:
让我们从一个两数相加的方法开始,但这个方法有一个转折。如果第一个或第二个数字是 42,则返回 42:
public class DataPipesSubject {
int addWithATwist(final int first, final int second) {
if (first == 42 || second == 42) {
return 42;
}
return first + second;
}
}
We want to test this method using various combinations of inputs.
我们希望使用各种输入组合来测试这种方法。
Let’s see how to write and evolve a simple test to feed our inputs via a data pipe.
让我们来看看如何编写和发展一个简单的测试,通过数据管道提供输入。
3. Preparing Our Data-Driven Test
3.准备数据驱动测试
Let’s create a test class with a test for a single scenario and then build on it to add data pipes:
让我们创建一个测试类,对单一场景进行测试,然后在此基础上添加数据管道:
First, let’s create our DataPipesTest class with the subject of our test:
首先,让我们创建带有测试主题的 DataPipesTest 类:
@Title("Test various ways of using data pipes")
class DataPipesTest extends Specification {
@Subject
def dataPipesSubject = new DataPipesSubject()
// ...
}
We’ve used Spock’s @Title annotation around the class to give ourselves some extra context for upcoming tests.
我们在类周围使用了Spock的@Title注解,以便为即将进行的测试提供一些额外的上下文。
We’ve also annotated the subject of our test with Spock’s @Subject annotation. Note that we should be careful to import our Subject from spock.lang rather than from javax.security.auth.
我们还使用 Spock的 @Subject 注释对测试主题进行了注解。请注意,我们应注意从 spock.lang 导入 Subject 而不是从 javax.security.auth. 导入。
Although not strictly necessary, this syntactic sugar helps us quickly identify what’s being tested.
虽然严格来说没有必要,但这种语法糖分可以帮助我们快速识别正在测试的内容。
Now let’s create a test with our first two inputs, 1 and 2, using Spock’s given/when/then syntax:
现在,让我们使用 Spock 的 given/when/then 语法,用前两个输入 1 和 2 创建一个测试:
def "given two numbers when we add them then our result is the sum of the inputs"() {
given: "some inputs"
def first = 1
def second = 2
and: "an expected result"
def expectedResult = 3
when: "we add them together"
def result = dataPipesSubject.addWithATwist(first, second)
then: "we get our expected answer"
result == expectedResult
}
To prepare our test for data pipes, let’s move our inputs from the given/and blocks into a where block:
为了让我们的测试为数据管道做好准备,让我们把输入从 given/and 块移到 where 块中:
def "given a where clause with our inputs when we add them then our result is the sum of the inputs"() {
when: "we add our inputs together"
def result = dataPipesSubject.addWithATwist(first, second)
then: "we get our expected answer"
result == expectedResult
where: "we have various inputs"
first = 1
second = 2
expectedResult = 3
}
Spock evaluates the where block and implicitly adds any variables as parameters to the test. So, Spock sees our method declaration like this:
Spock 会评估 where 块,并隐式地将任何变量作为参数添加到测试中。因此,Spock 会看到这样的方法声明:
def "given some declared method parameters when we add our inputs then those types are used"(int first, int second, int expectedResult)
Note that when we coerce our data into a specific type, we declare the type and variable as a method parameter.
请注意,当我们将数据强制转换为特定类型时,我们会将类型和变量声明为方法参数。
Since our test is very simple, let’s condense the when and then blocks into a single expect block:
由于我们的测试非常简单,因此让我们把 when 和 then 块压缩成一个 expect 块:
def "given an expect block to simplify our test when we add our inputs then our result is the sum of the two numbers"() {
expect: "our addition to get the right result"
dataPipesSubject.addWithATwist(first, second) == expectedResult
where: "we have various inputs"
first = 1
second = 2
expectedResult = 3
}
Now that we’ve simplified our test, we’re ready to add our first data pipe.
现在我们已经简化了测试,可以添加第一个数据管道了。
4. What Are Data Pipes?
4.什么是数据管道?
Data pipes in Spock are a way of feeding different combinations of data into our tests. This helps to keep our test code readable when we have more than one scenario to consider.
Spock 中的数据管道是一种将不同数据组合输入测试的方法。当我们需要考虑多个场景时,这有助于保持测试代码的可读性。
Pipes can be any Iterable – we can even create our own if it implements the Iterable interface!
管道可以是任何 Iterable – 如果它实现了 Iterable 接口,我们甚至可以创建自己的管道!
4.1. Simple Data Pipes
4.1.简单数据管道
Since arrays are Iterable, let’s start by converting our single inputs into arrays and using data pipes ‘<<‘ to feed them into our test:
由于数组是 Iterable 的,因此我们先将单个输入转换为数组,然后使用数据管道”<<“将它们输入到测试中:
where: "we have various inputs"
first << [1]
second << [2]
expectedResult << [3]
We can add additional test cases by adding entries to each array data pipe.
我们可以通过在每个数组数据管道中添加条目来增加测试用例。
So let’s add some data to our pipes for the scenarios 2 + 2 = 4 and 3 + 5 = 8:
因此,让我们为 2 + 2 = 4 和 3 + 5 = 8 的情况在管道中添加一些数据:
first << [1, 2, 3]
second << [2, 2, 5]
expectedResult << [3, 4, 8]
To make our test a bit more readable, let’s combine our first and second inputs into a multi-variable array data pipe, leaving our expectedResult separate for now:
为了使我们的测试更具可读性,让我们将 first 和 second 输入合并为一个多变量数组数据管道,暂时将 expectedResult 分开:
where: "we have various inputs"
[first, second] << [
[1, 2],
[2, 2],
[3, 5]
]
and: "an expected result"
expectedResult << [3, 4, 8]
Since we can refer to feeds that we’ve already defined, we could replace our expected result data pipe with the following:
由于我们可以引用已经定义的 feed,因此我们可以用下面的方法替换预期结果数据管道:
expectedResult = first + second
But let’s combine it with our input pipes since the method we’re testing has some subtleties that would break a simple addition:
但我们还是要把它与输入管道结合起来,因为我们正在测试的方法有一些微妙之处,会破坏简单的加法:
[first, second, expectedResult] << [
[1, 2, 3],
[2, 2, 4],
[3, 5, 8]
]
4.2. Maps and Methods
4.2.地图和方法
When we want more flexibility, and we’re using Spock 2.2 or later, we can feed our data using a Map as our data pipe:
如果我们需要更大的灵活性,并且我们使用的是 Spock 2.2 或更高版本,我们可以使用 Map 作为数据管道来输入数据:
where: "we have various inputs in the form of a map"
[first, second, expectedResult] << [
[
first : 1,
second: 2,
expectedResult: 3
],
[
first : 2,
second: 2,
expectedResult: 4
]
]
We can also pipe in our data from a separate method.
我们还可以从另一个方法中导入数据。
[first, second, expectedResult] << dataFeed()
Let’s see what our map data pipe looks like when we move it into a dataFeed method:
让我们看看当我们将地图数据管道移入 dataFeed 方法时,它是什么样子的:
def dataFeed() {
[
[
first : 1,
second: 2,
expectedResult: 3
],
[
first : 2,
second: 2,
expectedResult: 4
]
]
}
Although this approach works, using multiple inputs still feels clunky. Let’s look at how Spock’s Data Tables can improve this.
虽然这种方法行之有效,但使用多个输入仍然感觉笨拙。让我们看看 Spock 的数据表如何改善这种情况。
5. Data Tables
5.数据表
Spock’s data table format takes one or more data pipes, making them more visually appealing.
Spock 的数据表格式采用一个或多个数据管道,使其更具视觉吸引力。
Let’s rewrite the where block in our test method to use a data table instead of a collection of data pipes:
让我们重写测试方法中的 where 块,使用数据表而不是数据管道集合:
where: "we have various inputs"
first | second || expectedResult
1 | 2 || 3
2 | 2 || 4
3 | 5 || 8
So now, each row contains the inputs and expected results for a particular scenario, which makes our test scenarios much easier to read.
因此,现在每一行都包含了特定场景的输入和预期结果,这使得我们的测试场景更容易阅读。
As a visual cue and for best practice, we’ve used double ‘||’ to separate our inputs from our expected result.
作为视觉提示和最佳实践,我们使用双”||”来分隔输入和预期结果。
When we run our test with code coverage for these three iterations, we see that not all the lines of execution are covered. Our addWithATwist method has a special case when either input is 42:
当我们使用这三个迭代的代码覆盖率运行测试时,我们会发现并非所有的执行行都被覆盖。我们的 addWithATwist 方法在任一输入为 42 时都有一个特殊情况:
if (first == 42 || second == 42) {
return 42;
}
So, let’s add a scenario where our first input is 42, ensuring that our code executes the line inside our if statement. Let’s also add a scenario where our second input is 42 to ensure that our tests cover all the execution branches:
因此,让我们添加一个 first input 为 42 的场景,确保代码执行 if 语句中的行。我们还要添加一个 second 输入为 42 的场景,以确保我们的测试覆盖所有执行分支:
42 | 10 || 42
1 | 42 || 42
So here’s our final where block with iterations that give our code line and branch coverage:
这就是我们的最终 where 代码块,其中的迭代提供了代码行和分支覆盖率:
where: "we have various inputs"
first | second || expectedResult
1 | 2 || 3
2 | 2 || 4
3 | 5 || 8
42 | 10 || 42
1 | 42 || 42
When we execute these tests, our test runner renders a row for each iteration:
执行这些测试时,我们的测试运行程序会为每个迭代渲染一行:
DataPipesTest
- use table to supply the inputs
- use table to supply the inputs [first: 1, second: 2, expectedResult: 3, #0]
- use table to supply the inputs [first: 2, second: 2, expectedResult: 4, #1]
...
6. Readability Improvements
6.提高可读性
We have a few techniques that we can use to make our tests even more readable.
我们可以使用一些技巧来提高测试的可读性。
6.1. Inserting Variables Into Our Method Name
6.1.在方法名称中插入变量
When we want more expressive test executions, we can add variables to our method name.
当我们希望测试执行更具表现力时,可以在方法名称中添加变量。
So let’s enhance our test’s method name by inserting the column header variables from our table, prefixed with a ‘#’, and also add a scenario column:
因此,让我们通过插入表格中以 “#”为前缀的列头变量来增强测试的方法名称,同时添加一个场景列:
def "given a #scenario case when we add our inputs, #first and #second, then we get our expected result: #expectedResult"() {
expect: "our addition to get the right result"
dataPipesSubject.addWithATwist(first, second) == expectedResult
where: "we have various inputs"
scenario | first | second || expectedResult
"simple" | 1 | 2 || 3
"double 2" | 2 | 2 || 4
"special case" | 42 | 10 || 42
}
Now, when we run our test, our test runner renders the output as the more expressive:
现在,当我们运行测试时,我们的测试运行程序会将输出渲染为更具表现力的输出:
DataPipesTest
- given a #scenario case when we add our inputs, #first and #second, then we get our expected result: #expectedResult
- given a simple case when we add our inputs, 1 and 2, then we get our expected result: 3
- given a double 2 case when we add our inputs, 2 and 2, then we get our expected result: 4
...
When we use this approach but type the data pipe name incorrectly, Spock will fail the test with a message similar to this:
当我们使用这种方法,但输入的数据管道名称不正确时,Spock 会显示类似下面的信息,导致测试失败:
Error in @Unroll, could not find a matching variable for expression: myWrongVariableName
As before, we can reference our feeds in our table data using a feed we’ve already declared, even in the same row.
和以前一样,我们可以在表格数据中使用已声明的 feed 引用我们的 feed,即使是在同一行中。
So, let’s add a row that references our column header variables: first and second:
因此,让我们添加一行,引用我们的列标题变量:first 和 second:
scenario | first | second || expectedResult
"double 2 referenced" | 2 | first || first + second
6.2. When Table Columns Get Too Wide
6.2.当表格列过宽时
Our IDEs may contain intrinsic support for Spock’s tables – we can use IntelliJ’s “format code” feature (Ctrl+Alt+L) to align the columns in the table for us! Knowing this, we can add our data quickly without worrying about the layout and format it afterward.
我们的集成开发环境可能包含对 Spock 表格的内在支持–我们可以使用 IntelliJ 的 “格式化代码 “功能(Ctrl+Alt+L)来为我们对齐表格中的列!了解了这一点,我们就可以快速添加数据,而不必担心布局问题,并在之后对其进行格式化。
Sometimes, however, the length of data items in our tables causes a formatted table row to become too wide to fit on one line. Usually, that’s when we have Strings in our input.
但有时,表格中数据项的长度会导致格式化后的表格行过宽,无法在一行中显示。通常情况下,这就是输入字符串的情况。
To demonstrate this, let’s create a method that takes a String as an input and simply adds an exclamation mark:
为了演示这一点,让我们创建一个方法,将字符串作为输入,然后简单地添加一个感叹号:
String addExclamation(final String first) {
return first + '!';
}
Let’s now create a test with a long string as an input:
现在让我们创建一个以长字符串作为输入的测试:
def "given long strings when our tables our too big then we can use shared or static variables to shorten the table"() {
expect: "our addition to get the right result"
dataPipesSubject.addExclamation(longString) == expectedResult
where: "we have various inputs"
longString || expectedResult
'When we have a very long string we can use a static or @Shared variable to make our tables easier to read' || 'When we have a very long string we can use a static or @Shared variable to make our tables easier to read!'
}
Now, let’s make this table more compact by replacing the string with a static or @Shared variable. Note that our table can’t use variables declared in our test – our table can only use static, @Shared, or calculated values.
现在,让我们用静态变量或 @Shared 变量替换字符串,使表格更加紧凑。请注意,我们的表格不能使用在测试中声明的变量 – 我们的表格只能使用静态、@共享或计算值。
So, let’s declare a static and shared variable and use those in our table instead:
因此,让我们声明一个静态变量和共享变量,并在表格中使用它们:
static def STATIC_VARIABLE = 'When we have a very long string we can use a static variable'
@Shared
def SHARED_VARIABLE = 'When we have a very long string we can annotate our variable with @Shared'
...
scenario | longString || expectedResult
'use of static' | STATIC_VARIABLE || "$STATIC_VARIABLE!"
'use of @Shared' | SHARED_VARIABLE || "$SHARED_VARIABLE!"
Now our table is much more compact! We’ve also used Groovy’s String interpolation to expand the variables in our double-quoted strings in our expected result to show how that can help readability. Note that just using $ is enough for simple variable substitution, but for more complex cases we need to wrap our expression inside curly braces ${}.
现在,我们的表格更加紧凑了!我们还使用了 Groovy 的 String interpolation 来扩展预期结果中双引号字符串中的变量,以展示这如何有助于提高可读性。请注意,仅使用 $ 就足以实现简单的变量替换,但对于更复杂的情况,我们需要将表达式包在大括号 ${} 中。
Another way we can make a large table more readable is to split the table into multiple sections by using two or more underscores ‘__’:
另一种提高大表格可读性的方法是使用两个或多个下划线”__”将表格分割成多个部分:
where: "we have various inputs"
first | second
1 | 2
2 | 3
3 | 5
__
expectedResult | _
3 | _
5 | _
8 | _
Of course, we need to have the same number of rows across the split tables.
当然,我们需要在拆分的表中拥有相同数量的行。
Spock tables must have at least two columns, but after we split our table, expectedResult would have been on its own, so we’ve added an empty ‘_’ column to meet this requirement.
Spock 表必须至少有两列,但在我们拆分表后,expectedResult 将独立存在,因此我们添加了一个空的”_”列,以满足这一要求。
6.3. Alternative Table Separators
6.3.备选表格分隔符
Sometimes, we may not want to use ‘|’ as a separator. In such cases, we can use ‘;’ instead:
有时,我们可能不想使用”|”作为分隔符。在这种情况下,我们可以使用’;’来代替:
first ; second ;; expectedResult
1 ; 2 ;; 3
2 ; 3 ;; 5
3 ; 5 ;; 8
But we can’t mix and match both ‘|’ and ‘;’ column separators in the same table!
但是,我们不能在同一个表格中混合使用”|”和”; “这两种列分隔符!
7. Conclusion
7.结论
In this article, we learned how to use Spock’s data feeds in a where block. We’ve learned how data tables are a visually nicer representation of data feeds and how we can improve our test coverage by simply adding a row of data to a data table. We’ve also explored a few ways of making our data more readable, especially when dealing with large data values or when our tables get too big.
在本文中,我们学习了如何在 where 块中使用 Spock 的数据源。我们还学习了数据表如何以更直观的方式表示数据源,以及如何通过简单地向数据表中添加一行数据来提高测试覆盖率。我们还探索了一些使数据更具可读性的方法,尤其是在处理大数据值或表格过大时。
As usual, the source for this article can be found over on GitHub.