From 76d53f93007b0a9f875e19aee900b3dd25953480 Mon Sep 17 00:00:00 2001 From: AndreasTu Date: Fri, 18 Oct 2024 18:56:33 +0200 Subject: [PATCH] Clarified documentation about Iterable data providers and size call Added note to the documentation to clarify the usage of Iterables as data pipes and the used size() method. Fixes #2022 --- docs/data_driven_testing.adoc | 5 + docs/release_notes.adoc | 1 + .../datapipes/DataPipesIteratorSpec.groovy | 176 ++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 spock-specs/src/test/groovy/org/spockframework/datapipes/DataPipesIteratorSpec.groovy diff --git a/docs/data_driven_testing.adoc b/docs/data_driven_testing.adoc index 57c2710a4b..68747d3133 100644 --- a/docs/data_driven_testing.adoc +++ b/docs/data_driven_testing.adoc @@ -252,6 +252,11 @@ used as a data provider. This includes objects of type `Collection`, `String`, ` 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). +NOTE: Spock uses the `size()` method to calculate the amount of iterations, + except for data providers that implement `Iterator`, + so make sure `size()` is working efficient, or supply an `Iterator` if that is not possible. + + == Multi-Variable Data Pipes If a data provider returns multiple values per iteration (as an object that Groovy knows how to iterate over), diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc index a5915facbb..ebfbb70062 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -23,6 +23,7 @@ include::include.adoc[] ** This allows the use of these classes in an OSGi environment, where the class imports in the embedded spec are not visible to the Spock OSGi bundle ClassLoader * Fix mocking issue with the ByteBuddy MockMaker when using multiple classloaders in Java 21 spockIssue:2017[] * Fix mocking of final classes via `@SpringBean` and `@SpringSpy` spockIssue:1960[] +* Clarified documentation about data providers and `size()` calls spockIssue:2022[] == 2.4-M4 (2024-03-21) diff --git a/spock-specs/src/test/groovy/org/spockframework/datapipes/DataPipesIteratorSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/datapipes/DataPipesIteratorSpec.groovy new file mode 100644 index 0000000000..2e980fcdd0 --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/datapipes/DataPipesIteratorSpec.groovy @@ -0,0 +1,176 @@ +/* + * 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. + */ + +package org.spockframework.datapipes + +import org.spockframework.EmbeddedSpecification +import spock.lang.Issue +import spock.util.EmbeddedSpecRunner + +import static java.util.Objects.requireNonNull + +class DataPipesIteratorSpec extends EmbeddedSpecification { + + private static final ThreadLocal currentDataProvider = new ThreadLocal<>(); + + def cleanup() { + currentDataProvider.remove() + } + + def "Collection data provider will use size method to estimate number of iterations"() { + given: + def dataCollection = dataProvider(new TestDataCollection()) + + when: + def res = runIterations() + + then: + res.testsSucceededCount == 2 + dataCollection.iteratorCalls == 1 + dataCollection.sizeCalls == 2 + } + + @Issue("https://github.com/spockframework/spock/issues/2022") + def "Iterable uses the Groovy default size method to estimate number of iterations"() { + given: + def dataIterable = dataProvider(new TestDataIterable()) + + when: + def res = runIterations() + + then: + dataIterable.iteratorCalls == 3 + res.testsSucceededCount == 2 + } + + @Issue("https://github.com/spockframework/spock/issues/2022") + def "Iterable with size method shall use the size method to estimate number of iterations"() { + given: + def dataIterable = dataProvider(new TestDataIterableWithSize()) + + when: + def res = runIterations() + + then: + res.testsSucceededCount == 2 + dataIterable.iteratorCalls == 1 + dataIterable.sizeCalls == 2 + } + + @Issue("https://github.com/spockframework/spock/issues/2022") + def "Iterator shall be only called once"() { + given: + def dataIterator = dataProvider(new TestDataIterator()) + + when: + def res = runIterations() + + then: + res.testsSucceededCount == 2 + dataIterator.hasNextCalls == 3 + dataIterator.nextCalls == 1 + } + + private EmbeddedSpecRunner.SummarizedEngineExecutionResults runIterations() { + runner.runSpecBody """ +def "run"(Object input){ + expect: + input != null + + where: + //noinspection UnnecessaryQualifiedReference + input << org.spockframework.datapipes.DataPipesIteratorSpec.getDataProvider() +} +""" + } + + private static T dataProvider(T dataProvider) { + currentDataProvider.set(requireNonNull(dataProvider)) + return dataProvider + } + + static Object getDataProvider() { + def data = currentDataProvider.get() + assert data != null + return data + } + + private static class TestDataCollection extends AbstractCollection { + int iteratorCalls = 0 + int sizeCalls = 0 + + @Override + Iterator iterator() { + iteratorCalls++ + return ["Value"].iterator() + } + + @Override + int size() { + sizeCalls++ + return 1 + } + } + + private static class TestDataIterableWithSize implements Iterable { + int iteratorCalls = 0 + int sizeCalls = 0 + + int size() { + sizeCalls++ + return 1 + } + + @Override + Iterator iterator() { + iteratorCalls++ + return ["Value"].iterator() + } + } + + private static class TestDataIterable implements Iterable { + int iteratorCalls = 0 + + @Override + Iterator iterator() { + iteratorCalls++ + return ["Value"].iterator() + } + } + + private static class TestDataIterator implements Iterator { + int hasNextCalls = 0 + int nextCalls = 0 + + @Override + boolean hasNext() { + hasNextCalls++ + if (nextCalls == 0) { + return true + } + return false + } + + @Override + Object next() { + nextCalls++ + if (nextCalls == 1) { + return "Value" + } + throw new NoSuchElementException() + } + } +}