From 4e1a6031662fadce2a3eb71e45ab7f08e872fd84 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 10 Mar 2025 15:48:51 +0100 Subject: [PATCH 1/5] feat: stub confidence for testing --- .../com/spotify/confidence/Confidence.java | 8 +- .../spotify/confidence/ConfidenceStub.java | 91 +++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java diff --git a/sdk-java/src/main/java/com/spotify/confidence/Confidence.java b/sdk-java/src/main/java/com/spotify/confidence/Confidence.java index bb88de76..bcc62882 100644 --- a/sdk-java/src/main/java/com/spotify/confidence/Confidence.java +++ b/sdk-java/src/main/java/com/spotify/confidence/Confidence.java @@ -42,7 +42,9 @@ public abstract class Confidence implements EventSender, Closeable { protected Map context = Maps.newHashMap(); private static final Logger log = org.slf4j.LoggerFactory.getLogger(Confidence.class); - private Confidence() {} + protected Confidence() { + // Protected constructor to allow subclassing + } protected abstract ClientDelegate client(); @@ -217,13 +219,13 @@ public static Confidence.Builder builder(String clientSecret) { return new Confidence.Builder(clientSecret); } - private static class ClientDelegate implements FlagResolverClient, EventSenderEngine { + static class ClientDelegate implements FlagResolverClient, EventSenderEngine { private final Closeable closeable; private final FlagResolverClient flagResolverClient; private final EventSenderEngine eventSenderEngine; private String clientSecret; - private ClientDelegate( + ClientDelegate( Closeable closeable, FlagResolverClient flagResolverClient, EventSenderEngine eventSenderEngine, diff --git a/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java b/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java new file mode 100644 index 00000000..37eedf71 --- /dev/null +++ b/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java @@ -0,0 +1,91 @@ +package com.spotify.confidence; + +import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsResponse; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class ConfidenceStub extends Confidence { + + private ConfidenceStub() { + // Private constructor to prevent direct instantiation + } + + public static ConfidenceStub createStub() { + return new ConfidenceStub(); + } + + @Override + protected ClientDelegate client() { + // Return a mock or no-op client delegate + return new MockClientDelegate(); + } + + @Override + public T getValue(String key, T defaultValue) { + // Return a default or mock value + return defaultValue; + } + + @Override + public FlagEvaluation getEvaluation(String key, T defaultValue) { + // Return a mock FlagEvaluation + return new FlagEvaluation<>(defaultValue, "", "MOCK"); + } + + @Override + public CompletableFuture resolveFlags(String flagName) { + // Return a completed future with a mock response + return CompletableFuture.completedFuture(ResolveFlagsResponse.getDefaultInstance()); + } + + @Override + public void track(String eventName) { + // No-op for tracking + } + + @Override + public void track(String eventName, ConfidenceValue.Struct data) { + // No-op for tracking with data + } + + @Override + public void close() { + // No-op for close + } + + @Override + public void flush() { + // No-op for flush + } + + // Mock implementation of ClientDelegate + private static class MockClientDelegate extends ClientDelegate { + private MockClientDelegate() { + super(null, null, null, ""); + } + + @Override + public void emit( + String name, ConfidenceValue.Struct context, Optional message) { + // No-op + } + + @Override + public void flush() { + // No-op + } + + @Override + public CompletableFuture resolveFlags( + String flag, ConfidenceValue.Struct context) { + return CompletableFuture.completedFuture(ResolveFlagsResponse.getDefaultInstance()); + } + + @Override + public void close() { + // No-op + } + } + + // Additional methods to configure the stub can be added here +} From c0d73166f7684827f27cb9edffefe082b759c718 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 10 Mar 2025 16:02:08 +0100 Subject: [PATCH 2/5] feat: add ConfidenceStub for testing with configurable getValue and getEvaluation --- .../spotify/confidence/ConfidenceStub.java | 57 ++++++++++++++++++- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java b/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java index 37eedf71..761a48a1 100644 --- a/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java +++ b/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java @@ -1,11 +1,19 @@ package com.spotify.confidence; import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsResponse; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ConfidenceStub extends Confidence { + private final Map valueMap = new HashMap<>(); + private final Map evaluationConfigMap = new HashMap<>(); + private static final Logger log = LoggerFactory.getLogger(ConfidenceStub.class); + private ConfidenceStub() { // Private constructor to prevent direct instantiation } @@ -22,14 +30,30 @@ protected ClientDelegate client() { @Override public T getValue(String key, T defaultValue) { - // Return a default or mock value + // Check if a configured value exists + if (valueMap.containsKey(key)) { + Object value = valueMap.get(key); + if (defaultValue != null && defaultValue.getClass().isInstance(value)) { + return (T) value; + } else { + // Log a warning or throw an exception if the type doesn't match + log.warn("Type mismatch for key: " + key); + } + } + // Return the default value if not configured or type mismatch return defaultValue; } @Override public FlagEvaluation getEvaluation(String key, T defaultValue) { - // Return a mock FlagEvaluation - return new FlagEvaluation<>(defaultValue, "", "MOCK"); + // Use getValue to retrieve the configured value or default + T value = getValue(key, defaultValue); + // Retrieve additional configuration for FlagEvaluation + FlagEvaluationConfig config = + evaluationConfigMap.getOrDefault(key, new FlagEvaluationConfig("stub", "MOCK", null, null)); + // Return a FlagEvaluation with the retrieved value and additional fields + return new FlagEvaluation<>( + value, config.variant, config.reason, config.errorType, config.errorMessage); } @Override @@ -58,6 +82,18 @@ public void flush() { // No-op for flush } + // Method to configure return values + public void configureValue(String key, T value) { + valueMap.put(key, value); + } + + // Method to configure FlagEvaluation fields + public void configureEvaluationFields( + String key, String variant, String reason, ErrorType errorType, String errorMessage) { + evaluationConfigMap.put( + key, new FlagEvaluationConfig(variant, reason, errorType, errorMessage)); + } + // Mock implementation of ClientDelegate private static class MockClientDelegate extends ClientDelegate { private MockClientDelegate() { @@ -87,5 +123,20 @@ public void close() { } } + // Inner class to hold FlagEvaluation configuration + private static class FlagEvaluationConfig { + String variant; + String reason; + ErrorType errorType; + String errorMessage; + + FlagEvaluationConfig(String variant, String reason, ErrorType errorType, String errorMessage) { + this.variant = variant; + this.reason = reason; + this.errorType = errorType; + this.errorMessage = errorMessage; + } + } + // Additional methods to configure the stub can be added here } From ff463003677440c7ba7363136e47aeb4356a8da5 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 10 Mar 2025 16:14:32 +0100 Subject: [PATCH 3/5] feat: make API methods inspectable in ConfidenceStub --- .../spotify/confidence/ConfidenceStub.java | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java b/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java index 761a48a1..4099fe07 100644 --- a/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java +++ b/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java @@ -1,7 +1,9 @@ package com.spotify.confidence; import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsResponse; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -13,10 +15,9 @@ public class ConfidenceStub extends Confidence { private final Map valueMap = new HashMap<>(); private final Map evaluationConfigMap = new HashMap<>(); private static final Logger log = LoggerFactory.getLogger(ConfidenceStub.class); + private final List callHistory = new ArrayList<>(); - private ConfidenceStub() { - // Private constructor to prevent direct instantiation - } + private ConfidenceStub() {} public static ConfidenceStub createStub() { return new ConfidenceStub(); @@ -24,7 +25,6 @@ public static ConfidenceStub createStub() { @Override protected ClientDelegate client() { - // Return a mock or no-op client delegate return new MockClientDelegate(); } @@ -56,30 +56,24 @@ public FlagEvaluation getEvaluation(String key, T defaultValue) { value, config.variant, config.reason, config.errorType, config.errorMessage); } - @Override - public CompletableFuture resolveFlags(String flagName) { - // Return a completed future with a mock response - return CompletableFuture.completedFuture(ResolveFlagsResponse.getDefaultInstance()); - } - @Override public void track(String eventName) { - // No-op for tracking + logCall("track", eventName); } @Override public void track(String eventName, ConfidenceValue.Struct data) { - // No-op for tracking with data + logCall("track", eventName, data); } @Override public void close() { - // No-op for close + logCall("close"); } @Override public void flush() { - // No-op for flush + logCall("flush"); } // Method to configure return values @@ -94,6 +88,25 @@ public void configureEvaluationFields( key, new FlagEvaluationConfig(variant, reason, errorType, errorMessage)); } + // Method to log calls + private void logCall(String methodName, Object... args) { + StringBuilder logEntry = new StringBuilder(methodName + "("); + for (Object arg : args) { + logEntry.append(arg).append(", "); + } + if (args.length > 0) { + logEntry.setLength(logEntry.length() - 2); // Remove trailing comma and space + } + logEntry.append(")"); + callHistory.add(logEntry.toString()); + log.debug(logEntry.toString()); + } + + // Method to retrieve call history + public List getCallHistory() { + return new ArrayList<>(callHistory); + } + // Mock implementation of ClientDelegate private static class MockClientDelegate extends ClientDelegate { private MockClientDelegate() { From 4fb7ef52233e96e226eccd44a42dad88ab5fc28e Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 10 Mar 2025 16:23:51 +0100 Subject: [PATCH 4/5] fixup! feat: make API methods inspectable in ConfidenceStub --- .../main/java/com/spotify/confidence/ConfidenceStub.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java b/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java index 4099fe07..2b3518d8 100644 --- a/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java +++ b/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java @@ -32,7 +32,7 @@ protected ClientDelegate client() { public T getValue(String key, T defaultValue) { // Check if a configured value exists if (valueMap.containsKey(key)) { - Object value = valueMap.get(key); + final Object value = valueMap.get(key); if (defaultValue != null && defaultValue.getClass().isInstance(value)) { return (T) value; } else { @@ -47,9 +47,9 @@ public T getValue(String key, T defaultValue) { @Override public FlagEvaluation getEvaluation(String key, T defaultValue) { // Use getValue to retrieve the configured value or default - T value = getValue(key, defaultValue); + final T value = getValue(key, defaultValue); // Retrieve additional configuration for FlagEvaluation - FlagEvaluationConfig config = + final FlagEvaluationConfig config = evaluationConfigMap.getOrDefault(key, new FlagEvaluationConfig("stub", "MOCK", null, null)); // Return a FlagEvaluation with the retrieved value and additional fields return new FlagEvaluation<>( @@ -90,7 +90,7 @@ public void configureEvaluationFields( // Method to log calls private void logCall(String methodName, Object... args) { - StringBuilder logEntry = new StringBuilder(methodName + "("); + final StringBuilder logEntry = new StringBuilder(methodName + "("); for (Object arg : args) { logEntry.append(arg).append(", "); } From 6ba536427baf9e431886ea0f2bc25130b72fbcc7 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Mon, 10 Mar 2025 17:19:44 +0100 Subject: [PATCH 5/5] docs: add javadoc + readme --- README.md | 21 +++++++++++++++ .../spotify/confidence/ConfidenceStub.java | 26 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/README.md b/README.md index ffe4e7d3..bc6bd952 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,27 @@ Events are emitted to the Confidence backend: ```java confidence.track("my-event", ConfidenceValue.of(Map.of("field", ConfidenceValue.of("data")))); ``` +### Testing with ConfidenceStub + +For testing code that uses Confidence, we provide `ConfidenceStub` - a stub implementation that allows configuring predefined values and evaluation results for flags. It also tracks method calls and provides access to the call history for verification. + +Basic usage with predefined values: + +```java +// Create a ConfidenceStub instance +ConfidenceStub stub = ConfidenceStub.createStub(); + +// Configure a predefined value for a flag +stub.configureValue("flag-name.property-name", "predefinedValue"); + +// Retrieve the value using the stub +String value = stub.getValue("flag-name.property-name", "defaultValue"); +System.out.println("Retrieved value: " + value); + +// Verify the call history +List callHistory = stub.getCallHistory(); +System.out.println("Call history: " + callHistory); +``` ## OpenFeature The library includes a `Provider` for diff --git a/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java b/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java index 2b3518d8..55fef3f1 100644 --- a/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java +++ b/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java @@ -10,6 +10,32 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * A stub implementation of the Confidence class for testing purposes. + * + *

This class allows configuring predefined values and evaluation results for flags, making it + * useful for unit testing code that depends on Confidence feature flags. It tracks method calls and + * provides access to the call history for verification. + * + *

Example usage: + * + *

{@code
+ * ConfidenceStub stub = ConfidenceStub.createStub();
+ * stub.setValue("my-flag.boolean", true);
+ * boolean value = stub.getValue("my-flag.boolean", false); // Returns true
+ * }
+ * + *

The stub can also be configured with specific evaluation results: + * + *

{@code
+ * stub.setEvaluationConfig("my-flag.boolean", "variant-a", "RULE_MATCH");
+ * FlagEvaluation eval = stub.getEvaluation("my-flag.boolean", false);
+ * // eval contains the configured value, variant and reason
+ * }
+ * + * @see Confidence + * @see FlagEvaluation + */ public class ConfidenceStub extends Confidence { private final Map valueMap = new HashMap<>();