Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Derived data provider #554

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 39 additions & 26 deletions docs/data_driven_testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Suppose we want to specify the behavior of the +Math.max+ method:
[source,groovy]
----
class MathSpec extends Specification {
def "maximum of two numbers"() {
def 'maximum of two numbers'() {
expect:
// exercise math method for a few different inputs
Math.max(1, 3) == 3
Expand All @@ -35,7 +35,7 @@ hard-coded integer values:
[source,groovy]
----
class MathSpec extends Specification {
def "maximum of two numbers"(int a, int b, int c) {
def 'maximum of two numbers'(int a, int b, int c) {
expect:
Math.max(a, b) == c

Expand All @@ -55,7 +55,7 @@ Data tables are a convenient way to exercise a feature method with a fixed set o
[source,groovy]
----
class Math extends Specification {
def "maximum of two numbers"(int a, int b, int c) {
def 'maximum of two numbers'(int a, int b, int c) {
expect:
Math.max(a, b) == c

Expand All @@ -73,6 +73,15 @@ _table rows_, hold the corresponding values. For each row, the feature method wi
_iteration_ of the method. If an iteration fails, the remaining iterations will nevertheless be executed. All
failures will be reported.

Since version 1.1 data table cells can reference previous ones also (works for data pipes also), e.g.

[source,groovy]
----
where:
a | b
1 | a + 1
----

Data tables must have at least two columns. A single-column table can be written as:

[source,groovy]
Expand Down Expand Up @@ -104,17 +113,21 @@ spec, all of which can be kept in the same file. This achieves better isolation

The previous code can be tweaked in a few ways. First, since the `where:` block already declares all data variables, the
method parameters can be omitted.footnote:[The idea behind allowing method parameters is to enable better IDE support.
However, recent versions of IntelliJ IDEA recognize data variables automatically, and even infer their types from the
values contained in the data table.]
However, recent versions of IntelliJ IDEA recognize data variables automatically, and often infer their types from the
values contained in the data table. They might still be needed if you want `@CompileStatic` or `@TypeChecked` support.]

Since version 1.1 you can also declare the types of the data table variables selectively (in their declaration order),
omitting the obvious ones.

Second, inputs and expected outputs can be separated with a double pipe symbol (`||`) to visually set them apart.
With this, the code becomes:

[source,groovy]
----
class DataDriven extends Specification {
def "maximum of two numbers"() {
def 'maximum of two numbers'() {
expect:
Math.max(a, b) == c
max(a, b) == c

where:
a | b || c
Expand All @@ -135,7 +148,7 @@ maximum of two numbers FAILED

Condition not satisfied:

Math.max(a, b) == c
max(a, b) == c
| | | | |
| 7 0 | 7
42 false
Expand All @@ -154,7 +167,7 @@ A method annotated with `@Unroll` will have its iterations reported independentl
[source,groovy]
----
@Unroll
def "maximum of two numbers"() { ... }
def 'maximum of two numbers'() { ... }
----

.Why isn't `@Unroll` the default?
Expand All @@ -171,7 +184,7 @@ Depending on the execution environment, the output will look something like:
maximum of two numbers[0] PASSED
maximum of two numbers[1] FAILED

Math.max(a, b) == c
max(a, b) == c
| | | | |
| 7 0 | 7
42 false
Expand All @@ -184,7 +197,7 @@ This tells us that the second iteration (with index 1) failed. With a bit of eff
[source,groovy]
----
@Unroll
def "maximum of #a and #b is #c"() { ... }
def 'maximum of #a and #b is #c'() { ... }
----

This method name uses placeholders, denoted by a leading hash sign (`#`), to refer to data variables `a`, `b`,
Expand All @@ -194,7 +207,7 @@ and `c`. In the output, the placeholders will be replaced with concrete values:
maximum of 3 and 5 is 5 PASSED
maximum of 7 and 0 is 7 FAILED

Math.max(a, b) == c
max(a, b) == c
| | | | |
| 7 0 | 7
42 false
Expand All @@ -218,14 +231,14 @@ one or more _data pipes_:
...
where:
a << [3, 7, 0]
b << [5, 0, 0]
c << [5, 7, 0]
b << [5, 0, 1]
c << [a+1, b+1, a+b]
----

A data pipe, indicated by the left-shift (`<<`) operator, connects a data variable to a _data provider_. The data
provider holds all values for the variable, one per iteration. Any object that Groovy knows how to iterate over can be
used as a data provider. This includes objects of type `Collection`, `String`, `Iterable`, and objects implementing the
`Iterable` contract. Data providers don't necessarily have to _be_ the data (as in the case of a `Collection`);
used as a data provider. This includes objects implementing the `Iterable` contract.
Data providers don't necessarily have to _be_ the data (as in the case of a `Collection`);
they can fetch data from external sources like text files, databases and spreadsheets, or generate data randomly.
Data providers are queried for their next value only when needed (before the next iteration).

Expand All @@ -237,12 +250,12 @@ but uses brackets instead of parentheses on the left-hand side:

[source,groovy]
----
@Shared sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")
@Shared sql = Sql.newInstance('jdbc:h2:mem:', 'org.h2.Driver')

def "maximum of two numbers"() {
def 'maximum of two numbers'() {
...
where:
[a, b, c] << sql.rows("select a, b, c from maxdata")
[a, b, c] << sql.rows('select a, b, c from maxdata')
}
----

Expand All @@ -252,7 +265,7 @@ Data values that aren't of interest can be ignored with an underscore (`_`):
----
...
where:
[a, b, _, c] << sql.rows("select * from maxdata")
[a, b, _, c] << sql.rows('select * from maxdata')
----

== Data Variable Assignment
Expand All @@ -265,7 +278,7 @@ A data variable can be directly assigned a value:
where:
a = 3
b = Math.random() * 100
c = a > b ? a : b
c = (a > b) ? a : b
----

Assignments are re-evaluated for every iteration. As already shown above, the right-hand side of an assignment may refer
Expand All @@ -275,7 +288,7 @@ to other data variables:
----
...
where:
row << sql.rows("select * from maxdata")
row << sql.rows('select * from maxdata')
// pick apart columns
a = row.a
b = row.b
Expand Down Expand Up @@ -325,8 +338,8 @@ following are valid method names:

[source,groovy]
----
def "#person is #person.age years old"() { ... } // property access
def "#person.name.toUpperCase()"() { ... } // zero-arg method call
def '#person is #person.age years old'() { ... } // property access
def '#person.name.toUpperCase()'() { ... } // zero-arg method call
----

Non-string values (like `#person` above) are converted to Strings according to Groovy semantics.
Expand All @@ -336,14 +349,14 @@ The following are invalid method names:
[source,groovy]
----
def "#person.name.split(' ')[1]" { ... } // cannot have method arguments
def "#person.age / 2" { ... } // cannot use operators
def '#person.age / 2' { ... } // cannot use operators
----

If necessary, additional data variables can be introduced to hold more complex expression:

[source,groovy]
----
def "#lastName"() {
def '#lastName'() {
...
where:
person << ...
Expand Down
52 changes: 24 additions & 28 deletions spock-core/src/main/java/org/spockframework/compiler/AstUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,21 @@

package org.spockframework.compiler;

import java.util.*;

import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.*;
import org.codehaus.groovy.runtime.dgmimpl.arrays.*;

import org.codehaus.groovy.runtime.dgmimpl.arrays.IntegerArrayGetAtMetaMethod;
import org.objectweb.asm.Opcodes;

import org.spockframework.lang.Wildcard;
import org.spockframework.runtime.SpockRuntime;
import org.spockframework.util.Nullable;

import org.spockframework.util.ObjectUtil;
import org.spockframework.util.*;
import spock.lang.Specification;

import java.util.*;

/**
* Utility methods for AST processing.
*
*
* @author Peter Niederwieser
*/
public abstract class AstUtil {
Expand All @@ -43,7 +39,7 @@ public abstract class AstUtil {
/**
* Tells whether the given node has an annotation of the given type.
*
* @param node an AST node
* @param node an AST node
* @param annotationType an annotation type
* @return <tt>true</tt> iff the given node has an annotation of the given type
*/
Expand All @@ -54,7 +50,7 @@ public static boolean hasAnnotation(ASTNode node, Class<?> annotationType) {
public static AnnotationNode getAnnotation(ASTNode node, Class<?> annotationType) {
if (!(node instanceof AnnotatedNode)) return null;

AnnotatedNode annotated = (AnnotatedNode)node;
AnnotatedNode annotated = (AnnotatedNode) node;
@SuppressWarnings("unchecked")
List<AnnotationNode> annotations = annotated.getAnnotations();
for (AnnotationNode a : annotations)
Expand All @@ -77,15 +73,15 @@ public static List<Statement> getStatements(MethodNode method) {
if (code != null) block.addStatement(code);
method.setCode(block);
}
return ((BlockStatement)method.getCode()).getStatements();
return ((BlockStatement) method.getCode()).getStatements();
}

@SuppressWarnings("unchecked")
public static List<Statement> getStatements(ClosureExpression closure) {
BlockStatement blockStat = (BlockStatement)closure.getCode();
BlockStatement blockStat = (BlockStatement) closure.getCode();
return blockStat == null ?
Collections.<Statement> emptyList() : // it's not possible to add any statements to such a ClosureExpression, so immutable list is OK
blockStat.getStatements();
Collections.<Statement>emptyList() : // it's not possible to add any statements to such a ClosureExpression, so immutable list is OK
blockStat.getStatements();
}

public static boolean isInvocationWithImplicitThis(Expression invocation) {
Expand Down Expand Up @@ -162,7 +158,7 @@ public static boolean isSynthetic(MethodNode method) {
*/
public static boolean hasPlausibleSourcePosition(ASTNode node) {
return node.getLineNumber() > 0 && node.getLastLineNumber() >= node.getLineNumber()
&& node.getColumnNumber() > 0 && node.getLastColumnNumber() > node.getColumnNumber();
&& node.getColumnNumber() > 0 && node.getLastColumnNumber() > node.getColumnNumber();
}

public static String getMethodName(Expression invocation) {
Expand Down Expand Up @@ -229,13 +225,13 @@ public static Expression toArgumentArray(List<Expression> argList, IRewriteResou
return new ArrayExpression(ClassHelper.OBJECT_TYPE, argList);

return new MethodCallExpression(
new ClassExpression(resources.getAstNodeCache().SpockRuntime),
new ConstantExpression(SpockRuntime.DESPREAD_LIST),
new ArgumentListExpression(
new ArrayExpression(ClassHelper.OBJECT_TYPE, normalArgs),
new ArrayExpression(ClassHelper.OBJECT_TYPE, spreadArgs),
new ArrayExpression(ClassHelper.int_TYPE, spreadPositions)
));
new ClassExpression(resources.getAstNodeCache().SpockRuntime),
new ConstantExpression(SpockRuntime.DESPREAD_LIST),
new ArgumentListExpression(
new ArrayExpression(ClassHelper.OBJECT_TYPE, normalArgs),
new ArrayExpression(ClassHelper.OBJECT_TYPE, spreadArgs),
new ArrayExpression(ClassHelper.int_TYPE, spreadPositions)
));
}

public static void copySourcePosition(ASTNode from, ASTNode to) {
Expand All @@ -255,12 +251,12 @@ public static Expression getAssertionMessage(AssertStatement stat) {

public static boolean isThisExpression(Expression expr) {
return expr instanceof VariableExpression
&& ((VariableExpression) expr).isThisExpression();
&& ((VariableExpression) expr).isThisExpression();
}

public static boolean isSuperExpression(Expression expr) {
return expr instanceof VariableExpression
&& ((VariableExpression) expr).isSuperExpression();
&& ((VariableExpression) expr).isSuperExpression();
}

public static boolean isThisOrSuperExpression(Expression expr) {
Expand All @@ -274,7 +270,7 @@ public static void setVisibility(MethodNode method, int visibility) {
}

public static int getVisibility(FieldNode field) {
return field.getModifiers() & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE);
return field.getModifiers() & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE);
}

public static void setVisibility(FieldNode field, int visibility) {
Expand Down Expand Up @@ -316,10 +312,10 @@ public static void fixUpLocalVariables(Variable[] localVariables, VariableScope
fixUpLocalVariables(Arrays.asList(localVariables), scope, isClosureScope);
}

public static MethodCallExpression createGetAtMethod(Expression expression, int index) {
public static MethodCallExpression createGetAtMethod(Expression expression, Expression index) {
return new MethodCallExpression(expression,
GET_AT_METHOD_NAME,
new ConstantExpression(index));
index);
}

/**
Expand Down
Loading