From bf771f150f0922fa9ccb73abc45579e3942f52f4 Mon Sep 17 00:00:00 2001 From: Andreas Turban Date: Tue, 5 Nov 2024 18:38:59 +0100 Subject: [PATCH] Clarified documentation about Iterable data providers and size() calls (#2027) 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 | 174 ++++++++++++++++++ 3 files changed, 180 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 fee00b3f1d..d52a9c7324 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -25,6 +25,7 @@ include::include.adoc[] * Fix mocking of final classes via `@SpringBean` and `@SpringSpy` spockIssue:1960[] * Size of data providers is no longer calculated multiple times but only once * Fix exception when using `@RepeatUntilFailure` with a data provider with unknown iteration amount. spockPull:2031[] +* 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..946f39cc53 --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/datapipes/DataPipesIteratorSpec.groovy @@ -0,0 +1,174 @@ +/* + * 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 == 1 + } + + @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 == 2 + 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 == 1 + } + + @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.runFeatureBody """ + 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() + } + } +}