Skip to content

Commit

Permalink
Add verifyEach condition (#1887)
Browse files Browse the repository at this point in the history
Co-authored-by: Luke Daley <luke@gradle.com>
  • Loading branch information
leonard84 and ldaley committed Feb 26, 2024
1 parent 745275b commit b2433e8
Show file tree
Hide file tree
Showing 20 changed files with 677 additions and 130 deletions.
1 change: 1 addition & 0 deletions docs/include.adoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
:author: Peter Niederwieser, Leonard Brünings, The Spock Framework Team
:revnumber: X-replaced-by-gradle
:sourcedir: ../spock-specs/src/test/groovy/org/spockframework/docs
:snapshotdir: ../spock-specs/src/test/resources/org/spockframework/docs
:sourcedir-spring: ../spock-spring/src/test/groovy/org/spockframework/spring/docs
:resourcedir-spring: ../spock-spring/src/test/resources/org/spockframework/spring/docs
:sourcedir-spring-boot: ../spock-spring/boot2-test/src/test/groovy/org/spockframework/boot2
Expand Down
1 change: 1 addition & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ include::include.adoc[]
** Requires `org.mockito:mockito-core` >= 4.11 in the test class path
* Added support for mocking of static methods also for Java code with the new API `SpyStatic()` spockPull:1756[]
** The <<interaction-based-testing.adoc#static-mocks,static mock methods>> will delegate the creation to the mock makers
* Add <<spock_primer.adoc#verify-each,verifyEach>> method to perform assertions on each element of an `Iterable` spockPull:1887[]
* Add <<extensions.adoc#parameter-injection,annotation extensions for parameters>> spockPull:1599[]
* Add support for <<extensions.adoc#extension-store,keeping state in extensions>> spockPull:1692[]
* Add <<extensions.adoc#spock-interceptors,feature-scoped interceptors>> spockPull:1844[]
Expand Down
83 changes: 83 additions & 0 deletions docs/spock_primer.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,89 @@ or it can be used without a target.

Like `with` you can also optionally define a type hint for the IDE.

== Using `verifyEach` to assert on each element of an `Iterable`

There are several ways to do assertions on `Iterable` or `Collection`.

=== List Equality

List equality is useful when the expected values are few and can be easily constructed.

[source,groovy,indent=0]
----
include::{sourcedir}/primer/VerifyEachDocSpec.groovy[tag=simple-list-equality]
----

will be rendered as

----
include::{sourcedir}/primer/VerifyEachDocSpec.groovy[tag=simple-list-equality-result]
----

=== Set Equality

As with list equality, set equality is useful when the expected values are few and can be easily constructed.

[source,groovy,indent=0]
----
include::{sourcedir}/primer/VerifyEachDocSpec.groovy[tag=simple-set-equality]
----

will be rendered as

----
include::{sourcedir}/primer/VerifyEachDocSpec.groovy[tag=simple-set-equality-result]
----

=== Every Method
You can also use the standard Groovy method `every` to do assertions.
The downside is, that you don't get any insight into which element failed, or why.

[source,groovy,indent=0]
----
include::{sourcedir}/primer/VerifyEachDocSpec.groovy[tag=every-method]
----

will be rendered as

----
include::{sourcedir}/primer/VerifyEachDocSpec.groovy[tag=every-method-result]
----

[[verify-each]]
=== VerifyEach Method
Since 2.4, you can use the `verifyEach` method to perform assertions on every element of an iterable.
In contrast to `every`, it will not stop at the first failure and instead check every element and provide a comprehensive report at the end.
It won't render the full list of items, which makes it useful in cases of a large number of items.
Like `with` the current item will be set as the delegate and passed as parameter to the closure.
The statements in the closure will also be treated as implicit assertions.

[source,groovy,indent=0]
----
include::{sourcedir}/primer/VerifyEachDocSpec.groovy[tag=verify-each]
----

will be rendered as

----
include::{snapshotdir}/primer/VerifyEachDocSpec/verifyEach_method.txt[]
----

By default, the item will be rendered with a simple `toString()`.
However, you can provide a custom "namer"-method to provide your own representation.
This is useful when the item either has no useful `toString()` method or if it is too verbose.

[source,groovy,indent=0]
----
include::{sourcedir}/primer/VerifyEachDocSpec.groovy[tag=verify-each-namer]
----

will be rendered as

----
include::{snapshotdir}/primer/VerifyEachDocSpec/verifyEach_with_namer_method.txt[]
----

[[specifications_as_documentation]]
== Specifications as Documentation

Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,39 @@
/*
* Copyright 2009 the original author or authors.
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.spockframework.compiler;

import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.*;
import org.codehaus.groovy.classgen.BytecodeExpression;
import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.syntax.Types;
import org.spockframework.compat.groovy2.GroovyCodeVisitorCompat;
import org.spockframework.runtime.ValueRecorder;
import org.spockframework.util.*;
import org.spockframework.util.AbstractExpressionConverter;
import org.spockframework.util.Assert;
import org.spockframework.util.Identifiers;
import org.spockframework.util.TextUtil;

import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.*;
import org.codehaus.groovy.classgen.BytecodeExpression;
import org.codehaus.groovy.syntax.Types;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.spockframework.compiler.AstUtil.createDirectMethodCall;
Expand Down Expand Up @@ -611,7 +615,7 @@ private Statement rewriteCondition(Expression expr, Expression message, boolean
if (expr instanceof MethodCallExpression && !((MethodCallExpression) expr).isSpreadSafe()) {
MethodCallExpression methodCallExpression = (MethodCallExpression)expr;
String methodName = AstUtil.getMethodName(methodCallExpression);
if ((Identifiers.WITH.equals(methodName) || Identifiers.VERIFY_ALL.equals(methodName))) {
if ((Identifiers.CONDITION_METHODS.contains(methodName))) {
return surroundSpecialTryCatch(expr);
}
return rewriteMethodCondition(methodCallExpression, message, explicit, optOut);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
/*
* Copyright 2009 the original author or authors.
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.spockframework.compiler;

import org.spockframework.compiler.model.*;
import org.spockframework.util.*;

import java.util.*;

import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.*;
import org.codehaus.groovy.ast.stmt.AssertStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.syntax.Types;
import org.spockframework.compiler.model.*;
import org.spockframework.util.Identifiers;
import org.spockframework.util.Nullable;

import java.util.List;

import static java.util.Arrays.asList;
import static org.codehaus.groovy.ast.expr.MethodCallExpression.NO_ARGUMENTS;

/**
Expand Down Expand Up @@ -78,7 +78,7 @@ protected void doVisitExpressionStatement(ExpressionStatement stat) {
InteractionRewriter rewriter = visitInteractionAwareExpressionStatement(stat);

if (!pastSpecialMethodCallStats.contains(stat)
|| currSpecialMethodCall.isWithCall()
|| currSpecialMethodCall.isConditionMethodCall()
|| currSpecialMethodCall.isGroupConditionBlock()) {

boolean handled = handleInteraction(rewriter, stat);
Expand Down Expand Up @@ -192,7 +192,7 @@ private boolean handleInteraction(InteractionRewriter rewriter, ExpressionStatem

private boolean handleImplicitCondition(ExpressionStatement stat) {
if (!(stat == currTopLevelStat && isThenOrExpectBlock()
|| currSpecialMethodCall.isWithCall()
|| currSpecialMethodCall.isConditionMethodCall()
|| currSpecialMethodCall.isConditionBlock()
|| currSpecialMethodCall.isGroupConditionBlock()
|| (insideInteraction && interactionClosureDepth == 1))) {
Expand All @@ -202,18 +202,18 @@ private boolean handleImplicitCondition(ExpressionStatement stat) {

checkIsValidImplicitCondition(stat);

boolean withOrVerifyAll = Identifiers.WITH.equals(AstUtil.getMethodName(stat.getExpression()))
|| Identifiers.VERIFY_ALL.equals(AstUtil.getMethodName(stat.getExpression()));
String methodName = AstUtil.getMethodName(stat.getExpression());
boolean isConditionMethodCall = Identifiers.CONDITION_METHODS.contains(methodName);

if (withOrVerifyAll) {
if (isConditionMethodCall) {
groupConditionFound = currSpecialMethodCall.isGroupConditionBlock();
} else {
conditionFound();
}

if ((currSpecialMethodCall.isWithCall() || currSpecialMethodCall.isGroupConditionBlock())
if ((currSpecialMethodCall.isConditionMethodCall() || currSpecialMethodCall.isGroupConditionBlock())
&& AstUtil.isInvocationWithImplicitThis(stat.getExpression())
&& !withOrVerifyAll) {
&& !isConditionMethodCall) {
replaceObjectExpressionWithCurrentClosure(stat);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
/*
* Copyright 2012 the original author or authors.
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.spockframework.compiler;
Expand All @@ -34,6 +35,7 @@ public interface ISpecialMethodCall {
boolean isInteractionCall();

boolean isWithCall();
boolean isConditionMethodCall();

boolean isConditionBlock();

Expand All @@ -49,7 +51,7 @@ public interface ISpecialMethodCall {

boolean isInteractionCall(MethodCallExpression expr);

boolean isWithCall(MethodCallExpression expr);
boolean isConditionMethodCall(MethodCallExpression expr);

boolean isTestDouble(MethodCallExpression expr);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
/*
* Copyright 2012 the original author or authors.
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.spockframework.compiler;

import java.util.Collection;

import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.stmt.Statement;

import java.util.Collection;

public class NoSpecialMethodCall implements ISpecialMethodCall {
public static final ISpecialMethodCall INSTANCE = new NoSpecialMethodCall();

Expand Down Expand Up @@ -57,6 +59,11 @@ public boolean isWithCall() {
return false;
}

@Override
public boolean isConditionMethodCall() {
return false;
}

@Override
public boolean isConditionBlock() {
return false;
Expand Down Expand Up @@ -93,7 +100,7 @@ public boolean isInteractionCall(MethodCallExpression expr) {
}

@Override
public boolean isWithCall(MethodCallExpression expr) {
public boolean isConditionMethodCall(MethodCallExpression expr) {
return false;
}

Expand Down
Loading

0 comments on commit b2433e8

Please sign in to comment.