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 |