Skip to content

Commit 3e26f94

Browse files
committed
Introduce run ordering as a generic feature
Extending the initial implementation for run order randomisation, the former is not merely a special case of run ordering. We now have - DefaultSpecOrderer (basically a no-op) - RandomSpecOrderer, - AlphabeticalSpecOrderer, - AnnotatationBasedSpecOrderer with @order(int). Relates to #1443.
1 parent ebd2a3e commit 3e26f94

File tree

12 files changed

+600
-175
lines changed

12 files changed

+600
-175
lines changed

docs/extensions.adoc

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,17 @@ runner {
4444

4545
See the <<parallel_execution.adoc#parallel-execution,Parallel Execution>> section for a detailed description.
4646

47-
=== Random Test Order Configuration
47+
=== Test Order Configuration
4848

4949
[source,groovy]
5050
----
5151
runner {
52-
randomizeSpecRunOrder true // randomize specification run order
53-
randomizeFeatureRunOrder true // randomize feature run order within each specification
52+
orderer new RandomSpecOrderer() // randomize specification run order
5453
}
5554
----
5655

57-
See the <<#_randomize_run_order>> section for more details.
56+
Instead of the default run order, you can configure Spock to execute specifications and/or features e.g. in random,
57+
alphabetical or manually assigned, annotation-based order. See the <<#_run_order>> section for more details.
5858

5959
== Built-In Extensions
6060

@@ -721,21 +721,111 @@ runner {
721721
}
722722
----
723723

724-
=== Randomize Run Order
724+
=== Run Order
725725

726726
Ideally, automated tests in general and Spock specifications in particular should be independent of each other. The same
727-
applies to feature methods within a specification. One heuristic way to gain confidence that this is indeed the case, is
728-
to randomize the order in which specifications and/or features within each specification are executed.
727+
applies to feature methods within a specification. Therefore, you should not rely on any specific order of execution
728+
(run order).
729729

730-
By default, Spock executes both specifications and features in deterministic order, even though users should not rely on
731-
any particular run order. You can explicitly activate run order randomization separately for specifications and features
732-
or combine both randomization modes, using the <<spock-configuration-file,Spock Configuration File>>:
730+
Nevertheless, you have options to influence the run order, using the <<spock-configuration-file>> and a set of built-in
731+
orderers derived from super class `SpecOrderer`. Please check the Javadocs for package
732+
`org.spockframework.runtime.extension.builtin.orderer` for more details. You can also write your own `SpecOrderer`, if
733+
none of the built-in ones satisfies your needs.
734+
735+
Please note that `@Stepwise` always trumps any run order you might have configured, i.e. `@Stepwise` "wins" against
736+
`SpecOrderer`.
737+
738+
==== Random Run Order
739+
740+
One helpful way to heuristically increase your confidence that your tests are indeed independent of each other, is to
741+
explicitly say goodbye to deterministic run order by randomizing it.
742+
743+
[source,groovy]
744+
----
745+
import org.spockframework.runtime.extension.builtin.orderer.RandomSpecOrderer
746+
747+
runner {
748+
orderer new RandomSpecOrderer(
749+
true, // Randomize overall specification run order
750+
true, // Randomize the run order of feature methods within specifications
751+
System.currentTimeMillis() // Set a fixed value, if you want repeatable pseudo-random numbers.
752+
// This might be helpful for reproducing issues when debugging your tests.
753+
)
754+
}
755+
----
756+
757+
==== Alphabetical Run Order
758+
759+
Less useful than random run order, but available anyway, is a way to execute specifications and/or features
760+
alphabetically, based on their display names and a simple `String.compareTo(String)` (no fancy locale-based collation).
761+
The default sorting direction is ascending, optionally you can also sort elements in descending order.
733762

734763
[source,groovy]
735764
----
765+
import org.spockframework.runtime.extension.builtin.orderer.AlphabeticalSpecOrderer
766+
736767
runner {
737-
randomizeSpecRunOrder true // randomize specification run order
738-
randomizeFeatureRunOrder true // randomize feature run order within each specification
768+
orderer new AlphabeticalSpecOrderer(
769+
true, // Run specifications in alphabetical order by display name
770+
true, // Run feature methods within specifications in alphabetical order by display name
771+
false // Sort in ascending order (use 'true' for descending order)
772+
)
773+
}
774+
----
775+
776+
==== Annotation-Based Run Order
777+
778+
If you want to basically retain Spock's default run order for most or at least some of your specifications and/or
779+
feature methods, but modify it for particular specs/features, or take it to the extreme and manually assign run orders
780+
everywhere, use the `@Order(int)` annotation in combination with the annotation-based orderer:
781+
782+
[source,groovy]
783+
----
784+
import org.spockframework.runtime.extension.builtin.orderer.AnnotatationBasedSpecOrderer
785+
786+
runner {
787+
orderer new AnnotatationBasedSpecOrderer()
788+
}
789+
----
790+
791+
Please note, that `@Order` annotations have no effect whatsoever, if `AnnotatationBasedSpecOrderer` is not configured
792+
as the active orderer. E.g., you cannot expect to be able to use random ordering in combination with manually assigning
793+
run orders via annotations for some exceptions. Annotation-based ordering must be explicitly activated and is only
794+
available as a modification of Spock's default run order.
795+
796+
Using `@Order`, the basic idea is to assume unannotated specifications and features to all carry an implicit `@Order(0)`
797+
annotation. If you wish to run some specs/features before others, assign them a lower (negative) run order. If you want
798+
to run them after the default-ordered elements, assign them a higher (positive) order number:
799+
800+
[source,groovy]
801+
----
802+
@Order(1) // Execute after default-ordered specs
803+
class FirstSpec extends Specification {
804+
// Execute features in order 'three', 'one', 'two' in ascending order of assigned @Order values
805+
@Order(2) def one() { expect: true }
806+
@Order(3) def two() { expect: true }
807+
@Order(1) def three() { expect: true }
808+
}
809+
810+
@Order(-1) // Execute before default-ordered specs
811+
class SecondSpec extends Specification {
812+
def foo() { expect: true } // Default order
813+
@Order(99) def bar() { expect: true } // Execute after default-ordered features
814+
@Order(-5) def zot() { expect: true } // Execute before default-ordered features
815+
}
816+
817+
// Default order
818+
class ThirdSpec extends Specification {
819+
def "some feature"() { expect: true } // Default order
820+
@Order(1) def "another feature"() { expect: true } // Execute after default-ordered features
821+
def "one more feature"() { expect: true } // Default order
822+
}
823+
824+
// Default order
825+
class FourthSpec extends Specification {
826+
def 'feature X'() { expect: true } // Default order
827+
def 'feature M'() { expect: true } // Default order
828+
@Order(-1) def 'feature D'() { expect: true } // Execute before default-ordered features
739829
}
740830
----
741831

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,20 @@
1515
package org.spockframework.runtime.extension.builtin;
1616

1717
import org.spockframework.runtime.extension.IGlobalExtension;
18-
import org.spockframework.runtime.model.FeatureInfo;
1918
import org.spockframework.runtime.model.SpecInfo;
2019
import spock.config.RunnerConfiguration;
2120

22-
import java.util.Random;
23-
24-
public class RandomRunOrderExtension implements IGlobalExtension {
25-
private static final Random RANDOM = new Random();
21+
import java.util.Collection;
2622

23+
public class OrderExtension implements IGlobalExtension {
2724
private final RunnerConfiguration config;
2825

29-
public RandomRunOrderExtension(RunnerConfiguration config) {
26+
public OrderExtension(RunnerConfiguration config) {
3027
this.config = config;
3128
}
3229

3330
@Override
34-
public void visitSpec(SpecInfo spec) {
35-
if (config.randomizeSpecRunOrder)
36-
spec.setExecutionOrder(RANDOM.nextInt());
37-
if (config.randomizeFeatureRunOrder) {
38-
for (FeatureInfo featureInfo : spec.getAllFeatures())
39-
featureInfo.setExecutionOrder(RANDOM.nextInt());
40-
}
31+
public void initSpecs(Collection<SpecInfo> specs) {
32+
config.orderer.process(specs);
4133
}
4234
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.spockframework.runtime.extension.builtin.orderer;
2+
3+
import org.spockframework.runtime.model.SpecInfo;
4+
5+
import java.util.Collection;
6+
import java.util.concurrent.atomic.AtomicInteger;
7+
8+
public class AlphabeticalSpecOrderer extends SpecOrderer {
9+
private final boolean descending;
10+
11+
public AlphabeticalSpecOrderer(boolean orderSpecs, boolean orderFeatures, boolean descending) {
12+
super(orderSpecs, orderFeatures);
13+
this.descending = descending;
14+
}
15+
16+
public AlphabeticalSpecOrderer(boolean orderSpecs, boolean orderFeatures) {
17+
this(orderSpecs, orderFeatures, false);
18+
}
19+
20+
@Override
21+
protected void orderSpecs(Collection<SpecInfo> specs) {
22+
AtomicInteger i = new AtomicInteger();
23+
specs.stream()
24+
.sorted((o1, o2) -> descending
25+
? o2.getDisplayName().compareTo(o1.getDisplayName())
26+
: o1.getDisplayName().compareTo(o2.getDisplayName())
27+
)
28+
.forEach(specInfo -> specInfo.setExecutionOrder(i.getAndIncrement()));
29+
}
30+
31+
@Override
32+
protected void orderFeatures(Collection<SpecInfo> specs) {
33+
for (SpecInfo spec : specs) {
34+
AtomicInteger i = new AtomicInteger();
35+
spec.getAllFeatures().stream()
36+
.sorted((o1, o2) -> descending
37+
? o2.getDisplayName().compareTo(o1.getDisplayName())
38+
: o1.getDisplayName().compareTo(o2.getDisplayName())
39+
)
40+
.forEach(featureInfo -> featureInfo.setExecutionOrder(i.getAndIncrement()));
41+
}
42+
}
43+
44+
public boolean isDescending() {
45+
return descending;
46+
}
47+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.spockframework.runtime.extension.builtin.orderer;
2+
3+
import org.spockframework.runtime.model.FeatureInfo;
4+
import org.spockframework.runtime.model.SpecInfo;
5+
import spock.lang.Order;
6+
7+
import java.util.Collection;
8+
import java.util.concurrent.atomic.AtomicInteger;
9+
10+
public class AnnotatationBasedSpecOrderer extends SpecOrderer {
11+
public AnnotatationBasedSpecOrderer() {
12+
super(true, true);
13+
}
14+
15+
@Override
16+
protected void orderSpecs(Collection<SpecInfo> specs) {
17+
for (SpecInfo spec : specs) {
18+
Order orderAnnotation = spec.getAnnotation(Order.class);
19+
spec.setExecutionOrder(orderAnnotation == null ? 0 : orderAnnotation.value());
20+
}
21+
}
22+
23+
@Override
24+
protected void orderFeatures(Collection<SpecInfo> specs) {
25+
for (SpecInfo spec : specs) {
26+
for (FeatureInfo feature : spec.getAllFeatures()) {
27+
Order orderAnnotation = feature.getFeatureMethod().getAnnotation(Order.class);
28+
feature.setExecutionOrder(orderAnnotation == null ? 0 : orderAnnotation.value());
29+
}
30+
}
31+
}
32+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.spockframework.runtime.extension.builtin.orderer;
2+
3+
import org.spockframework.runtime.model.SpecInfo;
4+
5+
import java.util.Collection;
6+
7+
public class DefaultSpecOrderer extends SpecOrderer {
8+
public DefaultSpecOrderer() {
9+
super(false, false);
10+
}
11+
12+
@Override
13+
protected void orderSpecs(Collection<SpecInfo> specs) { }
14+
15+
@Override
16+
protected void orderFeatures(Collection<SpecInfo> specs) { }
17+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.spockframework.runtime.extension.builtin.orderer;
2+
3+
import org.spockframework.runtime.model.FeatureInfo;
4+
import org.spockframework.runtime.model.SpecInfo;
5+
6+
import java.util.Collection;
7+
import java.util.Random;
8+
import java.util.concurrent.atomic.AtomicInteger;
9+
10+
public class RandomSpecOrderer extends SpecOrderer {
11+
private final Random random;
12+
13+
public RandomSpecOrderer(boolean orderSpecs, boolean orderFeatures, long seed) {
14+
super(orderSpecs, orderFeatures);
15+
random = new Random(seed);
16+
}
17+
18+
public RandomSpecOrderer(boolean orderSpecs, boolean orderFeatures) {
19+
this(orderSpecs, orderFeatures, System.currentTimeMillis());
20+
}
21+
22+
public RandomSpecOrderer() {
23+
this(true, true);
24+
}
25+
26+
@Override
27+
protected void orderSpecs(Collection<SpecInfo> specs) {
28+
for (SpecInfo spec : specs)
29+
spec.setExecutionOrder(random.nextInt());
30+
}
31+
32+
@Override
33+
protected void orderFeatures(Collection<SpecInfo> specs) {
34+
for (SpecInfo spec : specs) {
35+
for (FeatureInfo feature : spec.getAllFeatures())
36+
feature.setExecutionOrder(random.nextInt());
37+
}
38+
}
39+
}

spock-core/src/main/java/spock/config/RunnerConfiguration.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,26 @@
1414

1515
package spock.config;
1616

17+
import org.spockframework.runtime.extension.builtin.orderer.DefaultSpecOrderer;
18+
import org.spockframework.runtime.extension.builtin.orderer.SpecOrderer;
19+
1720
/**
1821
* Configuration settings for the spec runner.
1922
*
2023
* <p>Example:
2124
* <pre>
25+
* import org.spockframework.runtime.extension.builtin.orderer.RandomSpecOrderer
2226
* import some.pkg.Fast
2327
* import some.pkg.IntegrationSpec
2428
*
2529
* runner {
26-
* include Fast // could be either an annotation or a (base) class
30+
* include Fast // could be either an annotation or a (base) class
2731
* exclude {
2832
* annotation some.pkg.Slow
2933
* baseClass IntegrationSpec
3034
* }
31-
* filterStackTrace true // this is the default
32-
* randomizeSpecRunOrder false // this is the default
33-
* randomizeFeatureRunOrder false // this is the default
35+
* filterStackTrace true // this is the default
36+
* orderer new RandomSpecOrderer() // DefaultSpecOrderer (no-op) is the default
3437
* }
3538
* </pre>
3639
*/
@@ -39,8 +42,7 @@ public class RunnerConfiguration {
3942
public IncludeExcludeCriteria include = new IncludeExcludeCriteria();
4043
public IncludeExcludeCriteria exclude = new IncludeExcludeCriteria();
4144
public ParallelConfiguration parallel = new ParallelConfiguration();
45+
public SpecOrderer orderer = new DefaultSpecOrderer();
4246
public boolean filterStackTrace = true;
4347
public boolean optimizeRunOrder = false;
44-
public boolean randomizeSpecRunOrder = false;
45-
public boolean randomizeFeatureRunOrder = false;
4648
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* https://www.apache.org/licenses/LICENSE-2.0
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package spock.lang;
16+
17+
import org.spockframework.runtime.extension.builtin.OrderExtension;
18+
import org.spockframework.runtime.extension.builtin.orderer.AnnotatationBasedSpecOrderer;
19+
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
/**
26+
* Assigns an execution order to a specification or feature.
27+
* <p>
28+
* Annotations of this type are picked up by the {@link OrderExtension}, if and only if the
29+
* {@link AnnotatationBasedSpecOrderer} is activated in the Spock configuration file: <pre>
30+
* runner {
31+
* orderer new AnnotatationBasedSpecOrderer()
32+
* }</pre>
33+
*/
34+
@Retention(RetentionPolicy.RUNTIME)
35+
@Target({ ElementType.TYPE, ElementType.METHOD })
36+
public @interface Order {
37+
int value();
38+
}

0 commit comments

Comments
 (0)