From 0e3367fb75bf81e86f3798995a4405c27667dacb Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Mon, 22 Apr 2024 18:22:35 -0700 Subject: [PATCH] Refactored SwitcherMock to SwitcherTest - Added AB test capability --- README.md | 34 +++++-- pom.xml | 8 +- .../switcherapi/client/SwitcherKey.java | 4 +- .../client/SwitcherMockExtension.java | 92 ------------------ .../switcherapi/client/SwitcherMockValue.java | 9 -- .../SwitcherTest.java} | 22 ++--- .../client/test/SwitcherTestExtension.java | 96 +++++++++++++++++++ .../client/test/SwitcherTestTemplate.java | 38 ++++++++ .../client/test/SwitcherTestValue.java | 9 ++ .../client/SwitcherBypassTest.java | 36 +++++-- 10 files changed, 217 insertions(+), 131 deletions(-) delete mode 100644 src/main/java/com/github/switcherapi/client/SwitcherMockExtension.java delete mode 100644 src/main/java/com/github/switcherapi/client/SwitcherMockValue.java rename src/main/java/com/github/switcherapi/client/{SwitcherMock.java => test/SwitcherTest.java} (55%) create mode 100644 src/main/java/com/github/switcherapi/client/test/SwitcherTestExtension.java create mode 100644 src/main/java/com/github/switcherapi/client/test/SwitcherTestTemplate.java create mode 100644 src/main/java/com/github/switcherapi/client/test/SwitcherTestValue.java diff --git a/README.md b/README.md index 3b2998d5..b0b81acf 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ https://github.com/switcherapi/switcher-api - Flexible and robust SDK that will keep your code clean and maintainable. - Able to work local using a snapshot file pulled from your remote Switcher-API Domain. - Silent mode is a hybrid configuration that automatically enables contingent sub-processes in case of any connectivity issue. -- Built-in mock implementation for clear and easy implementation of automated testing. +- Built-in test annotation for clear and easy implementation of automated testing. - Easy to setup. Switcher Context is responsible to manage all the configuration complexity between your application and API. # Usage @@ -241,8 +241,8 @@ MyAppFeatures.configure(ContextBuilder.builder() .snapshotLocation("/src/resources")); ``` -## Built-in mock feature -Write automated tests using this built-in mock mechanism to guide your test scenario according to what you want to test. +## Built-in test feature +Write automated tests using this built-in test annotation to guide your test scenario according to what you want to test.
*SwitcherExecutor* implementation has 2 methods that can make mock tests easier. Use assume to force a value to a switcher and forget to reset its original state. ```java @@ -269,13 +269,33 @@ void testSwitchers() { } ``` -#### SwitcherMock annotation - Requires JUnit 5 Jupiter -Predefine Switchers result outside your test methods via Parameterized Test. +#### SwitcherTest annotation - Requires JUnit 5 Jupiter +Predefine Switchers result outside your test methods with the SwitcherTest annotation.
It encapsulates the test and makes sure that the Switcher returns to its original state after concluding the test. +Simple use case (result is default to true, so it can be omitted): ```java -@SwitcherMock(key = MY_SWITCHER, result = true) +@SwitcherTest(key = MY_SWITCHER, result = true) void testMyFeature() { assertTrue(instance.myFeature()); } -``` \ No newline at end of file +``` + +Multiple Switchers where more than one Switcher is used in the test: +```java +@SwitcherTest(switchers = { + @SwitcherTestValue(key = MY_SWITCHER), + @SwitcherTestValue(key = MY_SWITCHER2) +}) +void testMyFeature() { + assertTrue(instance.myFeature()); +} +``` + +AB Test scenario where your test should return the same result regardless of the Switcher result: +```java +@SwitcherTest(key = MY_SWITCHER, abTest = true) +void testMyFeature() { + assertTrue(instance.myFeature()); +} +``` diff --git a/pom.xml b/pom.xml index 7c3f0161..c8f3982b 100644 --- a/pom.xml +++ b/pom.xml @@ -140,7 +140,7 @@ org.junit.jupiter junit-jupiter-params ${junit.version} - provided + test com.squareup.okhttp3 @@ -173,19 +173,19 @@ com.fasterxml.jackson.core jackson-annotations - 2.16.1 + 2.17.0 com.fasterxml.jackson.core jackson-databind - 2.16.1 + 2.17.0 com.fasterxml.jackson.module jackson-module-jakarta-xmlbind-annotations - 2.16.1 + 2.17.0 diff --git a/src/main/java/com/github/switcherapi/client/SwitcherKey.java b/src/main/java/com/github/switcherapi/client/SwitcherKey.java index 49e2bc16..2e613597 100644 --- a/src/main/java/com/github/switcherapi/client/SwitcherKey.java +++ b/src/main/java/com/github/switcherapi/client/SwitcherKey.java @@ -1,5 +1,7 @@ package com.github.switcherapi.client; +import com.github.switcherapi.client.test.SwitcherTest; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -15,7 +17,7 @@ * *

* The attribute name is used to be sent to the API and its value - * is used to work with {@link SwitcherMock} + * is used to work with {@link SwitcherTest} * * @author Roger Floriano (petruki) */ diff --git a/src/main/java/com/github/switcherapi/client/SwitcherMockExtension.java b/src/main/java/com/github/switcherapi/client/SwitcherMockExtension.java deleted file mode 100644 index 6d26a8f6..00000000 --- a/src/main/java/com/github/switcherapi/client/SwitcherMockExtension.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.github.switcherapi.client; - -import org.apache.commons.lang3.ArrayUtils; -import org.junit.jupiter.api.extension.AfterTestExecutionCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ExtensionContext.Namespace; -import org.junit.jupiter.api.extension.ExtensionContext.Store; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.support.AnnotationConsumer; - -import java.util.Arrays; -import java.util.Optional; -import java.util.stream.Stream; - -/** - * This extension implements a Parameterized Test that can mock Switcher results and - * reset after its conclusion - * - * @author Roger Floriano (petruki) - */ -class SwitcherMockExtension implements AfterTestExecutionCallback, - ArgumentsProvider, AnnotationConsumer { - - private static final String STORE_KEYS = "store_keys"; - private static final String STORE_KEY = "store_key"; - - private String key; - - private boolean result; - - private SwitcherMockValue[] switchers; - - @Override - public void accept(SwitcherMock switcherTester) { - this.key = switcherTester.key(); - this.result = switcherTester.result(); - this.switchers = switcherTester.switchers(); - } - - @Override - public Stream provideArguments(ExtensionContext context) { - if (ArrayUtils.isNotEmpty(switchers)) { - return provideMultipleArguments(context); - } - - return provideSingleArgument(context); - } - - @Override - public void afterTestExecution(ExtensionContext context) { - Optional parent = context.getParent(); - if (parent.isPresent()) { - Store store = getStore(parent.get()); - String[] keys = store.remove(STORE_KEYS, String[].class); - - if (ArrayUtils.isNotEmpty(keys)) { - for (String keyStored : keys) { - SwitcherExecutor.forget(keyStored); - } - } else { - String switcherKey = store.remove(STORE_KEY, String.class); - SwitcherExecutor.forget(switcherKey); - } - } - } - - private Stream provideMultipleArguments(ExtensionContext context) { - String[] keys = Arrays.stream(switchers) - .map(SwitcherMockValue::key) - .toArray(String[]::new); - - for (SwitcherMockValue value : switchers) { - SwitcherExecutor.assume(value.key(), value.result()); - } - - getStore(context).put(STORE_KEYS, keys); - return Stream.of(Arguments.of(keys, null)); - } - - private Stream provideSingleArgument(ExtensionContext context) { - SwitcherExecutor.assume(key, result); - getStore(context).put(STORE_KEY, key); - - return Stream.of(Arguments.of(key, null)); - } - - private Store getStore(ExtensionContext context) { - return context.getStore(Namespace.create(getClass(), context)); - } - -} diff --git a/src/main/java/com/github/switcherapi/client/SwitcherMockValue.java b/src/main/java/com/github/switcherapi/client/SwitcherMockValue.java deleted file mode 100644 index 0a5e4fd7..00000000 --- a/src/main/java/com/github/switcherapi/client/SwitcherMockValue.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.switcherapi.client; - -public @interface SwitcherMockValue { - - String key(); - - boolean result() default true; - -} diff --git a/src/main/java/com/github/switcherapi/client/SwitcherMock.java b/src/main/java/com/github/switcherapi/client/test/SwitcherTest.java similarity index 55% rename from src/main/java/com/github/switcherapi/client/SwitcherMock.java rename to src/main/java/com/github/switcherapi/client/test/SwitcherTest.java index 9ce51e2a..3fe57f5d 100644 --- a/src/main/java/com/github/switcherapi/client/SwitcherMock.java +++ b/src/main/java/com/github/switcherapi/client/test/SwitcherTest.java @@ -1,33 +1,33 @@ -package com.github.switcherapi.client; +package com.github.switcherapi.client.test; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; - /** * Annotate test cases with the Switcher Key and the expected result. * *

- * Requires JUnit 5 Jupiter @ParameterizedTest + * Requires JUnit 5 Jupiter * * @author Roger Floriano (petruki) */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) -@ArgumentsSource(SwitcherMockExtension.class) -@ExtendWith(SwitcherMockExtension.class) -@ParameterizedTest -public @interface SwitcherMock { +@ExtendWith(SwitcherTestExtension.class) +@TestTemplate +public @interface SwitcherTest { String key() default ""; boolean result() default true; - SwitcherMockValue[] switchers() default {}; + boolean abTest() default false; + + SwitcherTestValue[] switchers() default {}; } diff --git a/src/main/java/com/github/switcherapi/client/test/SwitcherTestExtension.java b/src/main/java/com/github/switcherapi/client/test/SwitcherTestExtension.java new file mode 100644 index 00000000..afee6203 --- /dev/null +++ b/src/main/java/com/github/switcherapi/client/test/SwitcherTestExtension.java @@ -0,0 +1,96 @@ +package com.github.switcherapi.client.test; + +import com.github.switcherapi.client.SwitcherExecutor; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.extension.*; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; + +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * This extension implements test template, before and after routines to mock Switcher results and + * reset after its conclusion + * + * @author Roger Floriano (petruki) + */ +class SwitcherTestExtension implements TestTemplateInvocationContextProvider, + AfterTestExecutionCallback, BeforeTestExecutionCallback { + + private static final String STORE_KEYS = "mock.keys"; + private static final String STORE_KEY = "mock.key"; + + private boolean abTest; + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return context.getRequiredTestMethod().isAnnotationPresent(SwitcherTest.class); + } + + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { + SwitcherTest switcherTest = context.getRequiredTestMethod().getAnnotation(SwitcherTest.class); + + if (switcherTest.abTest()) { + final SwitcherTestTemplate templateA = new SwitcherTestTemplate(switcherTest, true); + final SwitcherTestTemplate templateB = new SwitcherTestTemplate(switcherTest); + return Stream.of(templateA, templateB); + } + + final SwitcherTestTemplate template = new SwitcherTestTemplate(switcherTest); + return Stream.of(template); + } + + @Override + public void beforeTestExecution(ExtensionContext context) { + SwitcherTest switcherTest = context.getRequiredTestMethod().getAnnotation(SwitcherTest.class); + + if (switcherTest.abTest()) { + abTest = !abTest; + } + + if (ArrayUtils.isNotEmpty(switcherTest.switchers())) { + mockMultipleSwitchers(context, switcherTest, abTest); + } else { + mockSingleSwitcher(context, switcherTest, abTest); + } + } + + @Override + public void afterTestExecution(ExtensionContext context) { + Store store = getStore(context); + String[] keys = store.remove(STORE_KEYS, String[].class); + + if (ArrayUtils.isNotEmpty(keys)) { + for (String keyStored : keys) { + SwitcherExecutor.forget(keyStored); + } + } else { + String switcherKey = store.remove(STORE_KEY, String.class); + SwitcherExecutor.forget(switcherKey); + } + } + + private void mockMultipleSwitchers(ExtensionContext context, SwitcherTest switcherTest, boolean inverted) { + String[] keys = Arrays.stream(switcherTest.switchers()) + .map(SwitcherTestValue::key) + .toArray(String[]::new); + + for (SwitcherTestValue value : switcherTest.switchers()) { + SwitcherExecutor.assume(value.key(), inverted != value.result()); + } + + getStore(context).put(STORE_KEYS, keys); + } + + private void mockSingleSwitcher(ExtensionContext context, SwitcherTest switcherTest, boolean inverted) { + SwitcherExecutor.assume(switcherTest.key(), inverted != switcherTest.result()); + getStore(context).put(STORE_KEY, switcherTest.key()); + } + + private Store getStore(ExtensionContext context) { + return context.getStore(Namespace.create(getClass(), context)); + } + +} diff --git a/src/main/java/com/github/switcherapi/client/test/SwitcherTestTemplate.java b/src/main/java/com/github/switcherapi/client/test/SwitcherTestTemplate.java new file mode 100644 index 00000000..b8e9ddfa --- /dev/null +++ b/src/main/java/com/github/switcherapi/client/test/SwitcherTestTemplate.java @@ -0,0 +1,38 @@ +package com.github.switcherapi.client.test; + +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; + +import java.util.Arrays; + +class SwitcherTestTemplate implements TestTemplateInvocationContext { + + private static final String DISPLAY_NAME_TEMPLATE = "With %s as %s"; + + private final SwitcherTest switcherTest; + + private boolean inverted; + + SwitcherTestTemplate(SwitcherTest switcherTest, boolean inverted) { + this.switcherTest = switcherTest; + this.inverted = inverted; + } + + SwitcherTestTemplate(SwitcherTest switcherTest) { + this.switcherTest = switcherTest; + } + + @Override + public String getDisplayName(int invocationIndex) { + SwitcherTestValue[] switcherTestValues = switcherTest.switchers(); + + if (ArrayUtils.isNotEmpty(switcherTestValues)) { + return String.join(", ", Arrays.toString( + Arrays.stream(switcherTestValues) + .map(value -> String.format(DISPLAY_NAME_TEMPLATE, value.key(), inverted != value.result())) + .toArray())); + } + + return String.format(DISPLAY_NAME_TEMPLATE, switcherTest.key(), inverted != switcherTest.result()); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/switcherapi/client/test/SwitcherTestValue.java b/src/main/java/com/github/switcherapi/client/test/SwitcherTestValue.java new file mode 100644 index 00000000..a4c63c65 --- /dev/null +++ b/src/main/java/com/github/switcherapi/client/test/SwitcherTestValue.java @@ -0,0 +1,9 @@ +package com.github.switcherapi.client.test; + +public @interface SwitcherTestValue { + + String key(); + + boolean result() default true; + +} diff --git a/src/test/java/com/github/switcherapi/client/SwitcherBypassTest.java b/src/test/java/com/github/switcherapi/client/SwitcherBypassTest.java index 2eb42cba..8e3024f8 100644 --- a/src/test/java/com/github/switcherapi/client/SwitcherBypassTest.java +++ b/src/test/java/com/github/switcherapi/client/SwitcherBypassTest.java @@ -1,6 +1,8 @@ package com.github.switcherapi.client; import com.github.switcherapi.client.model.Switcher; +import com.github.switcherapi.client.test.SwitcherTest; +import com.github.switcherapi.client.test.SwitcherTestValue; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -10,8 +12,7 @@ import static com.github.switcherapi.Switchers.*; import static com.github.switcherapi.client.SwitcherContext.getSwitcher; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; class SwitcherBypassTest { @@ -91,7 +92,7 @@ void shouldReturnFalse_afterAssumingItsTrue() { assertFalse(switcher.isItOn()); } - @SwitcherMock(key = USECASE111, result = false) + @SwitcherTest(key = USECASE111, result = false) void shouldReturnFalse_usingParametrizedTest() { //given SwitcherContext.configure(ContextBuilder.builder().snapshotLocation(SNAPSHOTS_LOCAL).environment(FIXTURE2)); @@ -102,7 +103,7 @@ void shouldReturnFalse_usingParametrizedTest() { assertFalse(switcher.isItOn()); } - @SwitcherMock(key = USECASE111) + @SwitcherTest(key = USECASE111) void shouldReturnTrue_usingParametrizedTest() { //given SwitcherContext.configure(ContextBuilder.builder().snapshotLocation(SNAPSHOTS_LOCAL).environment(FIXTURE2)); @@ -113,9 +114,9 @@ void shouldReturnTrue_usingParametrizedTest() { assertTrue(switcher.isItOn()); } - @SwitcherMock(switchers = { - @SwitcherMockValue(key = USECASE111), - @SwitcherMockValue(key = USECASE112) + @SwitcherTest(switchers = { + @SwitcherTestValue(key = USECASE111), + @SwitcherTestValue(key = USECASE112) }) void shouldReturnTrue_usingParametrizedTestWithMultipleValues() { //given @@ -130,4 +131,25 @@ void shouldReturnTrue_usingParametrizedTestWithMultipleValues() { assertTrue(switcher.isItOn()); } + @SwitcherTest(key = USECASE111, abTest = true) + void shouldReturnSameResult_usingAbTest() { + assertEquals("Switcher key is " + USECASE111, workBothWay()); + } + + /** + * Fake scenario to test both ways of building a String. + * It is used to AB Test behavior when the same result is expected. + */ + private String workBothWay() { + Switcher switcher = getSwitcher(USECASE111); + + // Using String.format + if (switcher.isItOn()) { + return String.format("Switcher key is %s", USECASE111); + } + + // Using String concatenation + return "Switcher key is " + USECASE111; + } + }