From fd19e7e7eeffce8b46a8cd738857c5168f64f3c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Tue, 13 Jul 2021 22:51:21 +0100 Subject: [PATCH 1/4] Add CucumberSplit --- testing/pom.xml | 10 ++++ .../testing/cucumber/CucumberSplit.java | 51 +++++++++++++++++++ .../testing/cucumber/RunCucumberTest.java | 8 +++ .../testing/cucumber/StepDefinitions.java | 22 ++++++++ .../src/test/resources/cucumber.properties | 1 + .../client/testing/cucumber/Split.feature | 19 +++++++ 6 files changed, 111 insertions(+) create mode 100644 testing/src/main/java/io/split/client/testing/cucumber/CucumberSplit.java create mode 100644 testing/src/test/java/io/split/client/testing/cucumber/RunCucumberTest.java create mode 100644 testing/src/test/java/io/split/client/testing/cucumber/StepDefinitions.java create mode 100644 testing/src/test/resources/cucumber.properties create mode 100644 testing/src/test/resources/io/split/client/testing/cucumber/Split.feature 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..bd3b29a2e --- /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/RunCucumberTest.java b/testing/src/test/java/io/split/client/testing/cucumber/RunCucumberTest.java new file mode 100644 index 000000000..cbb714f82 --- /dev/null +++ b/testing/src/test/java/io/split/client/testing/cucumber/RunCucumberTest.java @@ -0,0 +1,8 @@ +package io.split.client.testing.cucumber; + +import io.cucumber.junit.Cucumber; +import org.junit.runner.RunWith; + +@RunWith(Cucumber.class) +public class RunCucumberTest { +} 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..559171086 --- /dev/null +++ b/testing/src/test/java/io/split/client/testing/cucumber/StepDefinitions.java @@ -0,0 +1,22 @@ +package io.split.client.testing.cucumber; + +import io.cucumber.java.Before; +import io.cucumber.java.Scenario; +import io.cucumber.java.en.Then; +import io.split.client.testing.SplitClientForTest; + +import static org.junit.Assert.assertEquals; + +public class StepDefinitions { + private final SplitClientForTest splitClient = new SplitClientForTest(); + + @Then("split {string} should be {string}") + public void split_should_be(String split, String expectedValue) { + assertEquals(expectedValue, splitClient.getTreatment("arbitraryKey", split)); + } + + @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/Split.feature b/testing/src/test/resources/io/split/client/testing/cucumber/Split.feature new file mode 100644 index 000000000..e41005a02 --- /dev/null +++ b/testing/src/test/resources/io/split/client/testing/cucumber/Split.feature @@ -0,0 +1,19 @@ +@split[cappuccino:off] +@split[dollars:on] +Feature: Split + This is an example of how to use Split with Cucumber + + @split[cappuccino:on] + @split[dollars:off] + Scenario: with cappuccino, but without dollars + Then split "cappuccino" should be "on" + And split "dollars" should be "off" + + Scenario: without cappuccino, but with dollars + Then split "cappuccino" should be "off" + And split "dollars" should be "on" + + @split[dollars:off] + Scenario: without cappuccino, nor dollars + Then split "cappuccino" should be "off" + And split "dollars" should be "off" From 87d6d4fe734db06882b5af08242a8892ccd8c733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Tue, 13 Jul 2021 23:05:58 +0100 Subject: [PATCH 2/4] Add URL in JavaDoc --- .../java/io/split/client/testing/cucumber/CucumberSplit.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index bd3b29a2e..716b51088 100644 --- a/testing/src/main/java/io/split/client/testing/cucumber/CucumberSplit.java +++ b/testing/src/main/java/io/split/client/testing/cucumber/CucumberSplit.java @@ -16,7 +16,7 @@ * configure a {@link SplitClientForTest} instance. *

*

- * To use it, define a Before Hook that invokes the {@link CucumberSplit#configureSplit(SplitClientForTest, Scenario)} + * To use it, define a Before Hook that invokes the {@link CucumberSplit#configureSplit(SplitClientForTest, Scenario)} * method. Example: *

* From 5024560a7d355c4eef9363a23c6836b2c19c063a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Sun, 18 Jul 2021 15:31:06 +0100 Subject: [PATCH 3/4] Improve the Cucumber example to be more realistic --- .../testing/cucumber/CoffeeMachine.java | 44 +++++++++++++++++++ .../testing/cucumber/RunCucumberTest.java | 1 + .../io/split/client/testing/cucumber/SKU.java | 37 ++++++++++++++++ .../testing/cucumber/StepDefinitions.java | 39 ++++++++++++++-- .../testing/cucumber/MakeCoffee.feature | 25 +++++++++++ .../client/testing/cucumber/Split.feature | 19 -------- 6 files changed, 143 insertions(+), 22 deletions(-) create mode 100644 testing/src/test/java/io/split/client/testing/cucumber/CoffeeMachine.java create mode 100644 testing/src/test/java/io/split/client/testing/cucumber/SKU.java create mode 100644 testing/src/test/resources/io/split/client/testing/cucumber/MakeCoffee.feature delete mode 100644 testing/src/test/resources/io/split/client/testing/cucumber/Split.feature 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 index cbb714f82..18a325ab7 100644 --- a/testing/src/test/java/io/split/client/testing/cucumber/RunCucumberTest.java +++ b/testing/src/test/java/io/split/client/testing/cucumber/RunCucumberTest.java @@ -3,6 +3,7 @@ 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 index 559171086..59d3eae29 100644 --- a/testing/src/test/java/io/split/client/testing/cucumber/StepDefinitions.java +++ b/testing/src/test/java/io/split/client/testing/cucumber/StepDefinitions.java @@ -1,18 +1,51 @@ 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("split {string} should be {string}") - public void split_should_be(String split, String expectedValue) { - assertEquals(expectedValue, splitClient.getTreatment("arbitraryKey", split)); + @Then("no drinks should be available") + public void no_drinks_should_be_available() { + List availableSKUs = coffeeMachine.getAvailableDrinks(); + assertEquals(emptyList(), availableSKUs); } @Before 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..5ca4f3351 --- /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 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 | diff --git a/testing/src/test/resources/io/split/client/testing/cucumber/Split.feature b/testing/src/test/resources/io/split/client/testing/cucumber/Split.feature deleted file mode 100644 index e41005a02..000000000 --- a/testing/src/test/resources/io/split/client/testing/cucumber/Split.feature +++ /dev/null @@ -1,19 +0,0 @@ -@split[cappuccino:off] -@split[dollars:on] -Feature: Split - This is an example of how to use Split with Cucumber - - @split[cappuccino:on] - @split[dollars:off] - Scenario: with cappuccino, but without dollars - Then split "cappuccino" should be "on" - And split "dollars" should be "off" - - Scenario: without cappuccino, but with dollars - Then split "cappuccino" should be "off" - And split "dollars" should be "on" - - @split[dollars:off] - Scenario: without cappuccino, nor dollars - Then split "cappuccino" should be "off" - And split "dollars" should be "off" From 77b4753709dd08c1b4051387b184f3647b6f6525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Sun, 18 Jul 2021 15:33:45 +0100 Subject: [PATCH 4/4] disambiguation --- .../io/split/client/testing/cucumber/MakeCoffee.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 5ca4f3351..bb2c98a15 100644 --- a/testing/src/test/resources/io/split/client/testing/cucumber/MakeCoffee.feature +++ b/testing/src/test/resources/io/split/client/testing/cucumber/MakeCoffee.feature @@ -1,7 +1,7 @@ # 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 describes how the coffee machine works. + The scenarios in this feature file describes how the coffee machine works. Scenario: Empty machine Given the machine is empty