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/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..55fef3f1 --- /dev/null +++ b/sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java @@ -0,0 +1,181 @@ +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; +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<>(); + private final Map evaluationConfigMap = new HashMap<>(); + private static final Logger log = LoggerFactory.getLogger(ConfidenceStub.class); + private final List callHistory = new ArrayList<>(); + + private ConfidenceStub() {} + + public static ConfidenceStub createStub() { + return new ConfidenceStub(); + } + + @Override + protected ClientDelegate client() { + return new MockClientDelegate(); + } + + @Override + public T getValue(String key, T defaultValue) { + // Check if a configured value exists + if (valueMap.containsKey(key)) { + final 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) { + // Use getValue to retrieve the configured value or default + final T value = getValue(key, defaultValue); + // Retrieve additional configuration for FlagEvaluation + 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<>( + value, config.variant, config.reason, config.errorType, config.errorMessage); + } + + @Override + public void track(String eventName) { + logCall("track", eventName); + } + + @Override + public void track(String eventName, ConfidenceValue.Struct data) { + logCall("track", eventName, data); + } + + @Override + public void close() { + logCall("close"); + } + + @Override + public void flush() { + logCall("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)); + } + + // Method to log calls + private void logCall(String methodName, Object... args) { + final 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() { + 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 + } + } + + // 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 +}