diff --git a/testing/pom.xml b/testing/pom.xml
index bbad31d7f..d690ce04f 100644
--- a/testing/pom.xml
+++ b/testing/pom.xml
@@ -23,5 +23,15 @@
junit
junit
+
+ io.cucumber
+ cucumber-java
+ 6.10.4
+
+
+ io.cucumber
+ cucumber-junit
+ 6.10.4
+
diff --git a/testing/src/main/java/io/split/client/testing/cucumber/CucumberSplit.java b/testing/src/main/java/io/split/client/testing/cucumber/CucumberSplit.java
new file mode 100644
index 000000000..716b51088
--- /dev/null
+++ b/testing/src/main/java/io/split/client/testing/cucumber/CucumberSplit.java
@@ -0,0 +1,51 @@
+package io.split.client.testing.cucumber;
+
+import io.cucumber.java.Scenario;
+import io.split.client.testing.SplitClientForTest;
+
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ *
+ * Simple Cucumber plugin for Split.
+ *
+ *
+ * Cucumber scenarios annotated with {@code @split[feature:treatment]} tags can be used to
+ * configure a {@link SplitClientForTest} instance.
+ *
+ *
+ * To use it, define a Before Hook that invokes the {@link CucumberSplit#configureSplit(SplitClientForTest, Scenario)}
+ * method. Example:
+ *
+ *
+ *
+ * import io.cucumber.java.Before;
+ * import io.split.client.testing.SplitClientForTest;
+ *
+ * public class StepDefinitions {
+ * private final SplitClientForTest splitClient = new SplitClientForTest();
+ *
+ * @Before
+ * public void configureSplit(Scenario scenario) {
+ * CucumberSplit.configureSplit(splitClient, scenario);
+ * }
+ * }
+ *
+ */
+public class CucumberSplit {
+ private static final Pattern SPLIT_TAG_PATTERN = Pattern.compile("^@split\\[(.*):(.*)]");
+
+ public static void configureSplit(SplitClientForTest splitClient, Scenario scenario) {
+ Collection tags = scenario.getSourceTagNames();
+ for (String tag : tags) {
+ Matcher matcher = SPLIT_TAG_PATTERN.matcher(tag);
+ if (matcher.matches()) {
+ String feature = matcher.group(1);
+ String treatment = matcher.group(2);
+ splitClient.registerTreatment(feature, treatment);
+ }
+ }
+ }
+}
diff --git a/testing/src/test/java/io/split/client/testing/cucumber/CoffeeMachine.java b/testing/src/test/java/io/split/client/testing/cucumber/CoffeeMachine.java
new file mode 100644
index 000000000..2cfa2e913
--- /dev/null
+++ b/testing/src/test/java/io/split/client/testing/cucumber/CoffeeMachine.java
@@ -0,0 +1,44 @@
+package io.split.client.testing.cucumber;
+
+import io.split.client.SplitClient;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.unmodifiableList;
+
+/**
+ * A simple coffee machine that displays available drinks. It can offer an experimental cappuccino
+ * drink that is toggled on/off with Split.
+ */
+public class CoffeeMachine {
+ private final SplitClient splitClient;
+ private final String splitKey;
+ private double level;
+
+ public CoffeeMachine(SplitClient splitClient, String splitKey) {
+ this.splitClient = splitClient;
+ this.splitKey = splitKey;
+ }
+
+ /**
+ * Indicate how full the machine is
+ *
+ * @param level a number between 0 and 1
+ */
+ public void setLevel(double level) {
+ this.level = level;
+ }
+
+ public List getAvailableDrinks() {
+ if(this.level == 0) return emptyList();
+
+ List availableDrinks = new ArrayList<>();
+ availableDrinks.add(new SKU("filter coffee", 0.80));
+ if ("on".equals(this.splitClient.getTreatment(splitKey, "cappuccino"))) {
+ availableDrinks.add(new SKU("cappuccino", 1.10));
+ }
+ return unmodifiableList(availableDrinks);
+ }
+}
diff --git a/testing/src/test/java/io/split/client/testing/cucumber/RunCucumberTest.java b/testing/src/test/java/io/split/client/testing/cucumber/RunCucumberTest.java
new file mode 100644
index 000000000..18a325ab7
--- /dev/null
+++ b/testing/src/test/java/io/split/client/testing/cucumber/RunCucumberTest.java
@@ -0,0 +1,9 @@
+package io.split.client.testing.cucumber;
+
+import io.cucumber.junit.Cucumber;
+import org.junit.runner.RunWith;
+
+// This is the entry point for Cucumber, which runs all the .feature files in the same package
+@RunWith(Cucumber.class)
+public class RunCucumberTest {
+}
diff --git a/testing/src/test/java/io/split/client/testing/cucumber/SKU.java b/testing/src/test/java/io/split/client/testing/cucumber/SKU.java
new file mode 100644
index 000000000..12e5a8cb1
--- /dev/null
+++ b/testing/src/test/java/io/split/client/testing/cucumber/SKU.java
@@ -0,0 +1,37 @@
+package io.split.client.testing.cucumber;
+
+import java.util.Objects;
+
+/**
+ * A simple Stock Keeping Unit (SKU).
+ */
+public class SKU {
+ private final String name;
+ private final double price;
+
+ public SKU(String name, double price) {
+ this.name = name;
+ this.price = price;
+ }
+
+ @Override
+ public String toString() {
+ return "SKU{" +
+ "name='" + name + '\'' +
+ ", price=" + price +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SKU sku = (SKU) o;
+ return Double.compare(sku.price, price) == 0 && name.equals(sku.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, price);
+ }
+}
diff --git a/testing/src/test/java/io/split/client/testing/cucumber/StepDefinitions.java b/testing/src/test/java/io/split/client/testing/cucumber/StepDefinitions.java
new file mode 100644
index 000000000..59d3eae29
--- /dev/null
+++ b/testing/src/test/java/io/split/client/testing/cucumber/StepDefinitions.java
@@ -0,0 +1,55 @@
+package io.split.client.testing.cucumber;
+
+import io.cucumber.java.Before;
+import io.cucumber.java.DataTableType;
+import io.cucumber.java.Scenario;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.split.client.testing.SplitClientForTest;
+
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Collections.emptyList;
+import static org.junit.Assert.assertEquals;
+
+public class StepDefinitions {
+ private final SplitClientForTest splitClient = new SplitClientForTest();
+ private final CoffeeMachine coffeeMachine = new CoffeeMachine(splitClient, "arbitraryKey");
+
+ // Called by Cucumber to convert each row in the data table in the .feature file to a SKU object
+ @DataTableType
+ public SKU sku(Map entry) {
+ return new SKU(
+ entry.get("name"),
+ Double.parseDouble(entry.get("price"))
+ );
+ }
+
+ @Given("the machine is not empty")
+ public void the_machine_is_not_empty() {
+ coffeeMachine.setLevel(1.0);
+ }
+
+ @Given("the machine is empty")
+ public void the_machine_is_empty() {
+ coffeeMachine.setLevel(0);
+ }
+
+ @Then("the following drinks should be available:")
+ public void the_following_drinks_should_be_available(List expectedSKUs) {
+ List availableSKUs = coffeeMachine.getAvailableDrinks();
+ assertEquals(expectedSKUs, availableSKUs);
+ }
+
+ @Then("no drinks should be available")
+ public void no_drinks_should_be_available() {
+ List availableSKUs = coffeeMachine.getAvailableDrinks();
+ assertEquals(emptyList(), availableSKUs);
+ }
+
+ @Before
+ public void configureSplit(Scenario scenario) {
+ CucumberSplit.configureSplit(splitClient, scenario);
+ }
+}
diff --git a/testing/src/test/resources/cucumber.properties b/testing/src/test/resources/cucumber.properties
new file mode 100644
index 000000000..b48dd63bf
--- /dev/null
+++ b/testing/src/test/resources/cucumber.properties
@@ -0,0 +1 @@
+cucumber.publish.quiet=true
diff --git a/testing/src/test/resources/io/split/client/testing/cucumber/MakeCoffee.feature b/testing/src/test/resources/io/split/client/testing/cucumber/MakeCoffee.feature
new file mode 100644
index 000000000..bb2c98a15
--- /dev/null
+++ b/testing/src/test/resources/io/split/client/testing/cucumber/MakeCoffee.feature
@@ -0,0 +1,25 @@
+# This tag is inherited by all the scenarios, setting the "cappuccino" split feature to "off" by default.
+@split[cappuccino:off]
+Feature: Make Coffee
+ The scenarios in this feature file describes how the coffee machine works.
+
+ Scenario: Empty machine
+ Given the machine is empty
+ Then no drinks should be available
+
+ Scenario: Display available drinks
+ Given the machine is not empty
+ Then the following drinks should be available:
+ | name | price |
+ | filter coffee | 0.80 |
+
+ # The tags on this scenario will be ["@split[cappuccino:off]", "@split[cappuccino:on]"]
+ # The @split tags are processed sequentially, so the cappuccino split feature will be set to "off"
+ # and then immediately overwritten to "on".
+ @split[cappuccino:on]
+ Scenario: Display available drinks (including the new experimental cappuccino)
+ Given the machine is not empty
+ Then the following drinks should be available:
+ | name | price |
+ | filter coffee | 0.80 |
+ | cappuccino | 1.10 |