Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
</br>*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
Expand All @@ -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.
</br>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());
}
```
```

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());
}
```
8 changes: 4 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>provided</scope>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
Expand Down Expand Up @@ -173,19 +173,19 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.16.1</version>
<version>2.17.0</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
<version>2.17.0</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jakarta-xmlbind-annotations</artifactId>
<version>2.16.1</version>
<version>2.17.0</version>
</dependency>
</dependencies>
</dependencyManagement>
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/github/switcherapi/client/SwitcherKey.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,7 +17,7 @@
*
* <p>
* 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)
*/
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* <b>Requires JUnit 5 Jupiter @ParameterizedTest</b>
* <b>Requires JUnit 5 Jupiter</b>
*
* @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 {};

}
Original file line number Diff line number Diff line change
@@ -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<TestTemplateInvocationContext> 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));
}

}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading