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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> callHistory = stub.getCallHistory();
System.out.println("Call history: " + callHistory);
```

## OpenFeature
The library includes a `Provider` for
Expand Down
8 changes: 5 additions & 3 deletions sdk-java/src/main/java/com/spotify/confidence/Confidence.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ public abstract class Confidence implements EventSender, Closeable {
protected Map<String, ConfidenceValue> 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();

Expand Down Expand Up @@ -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,
Expand Down
181 changes: 181 additions & 0 deletions sdk-java/src/main/java/com/spotify/confidence/ConfidenceStub.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>Example usage:
*
* <pre>{@code
* ConfidenceStub stub = ConfidenceStub.createStub();
* stub.setValue("my-flag.boolean", true);
* boolean value = stub.getValue("my-flag.boolean", false); // Returns true
* }</pre>
*
* <p>The stub can also be configured with specific evaluation results:
*
* <pre>{@code
* stub.setEvaluationConfig("my-flag.boolean", "variant-a", "RULE_MATCH");
* FlagEvaluation<Boolean> eval = stub.getEvaluation("my-flag.boolean", false);
* // eval contains the configured value, variant and reason
* }</pre>
*
* @see Confidence
* @see FlagEvaluation
*/
public class ConfidenceStub extends Confidence {

private final Map<String, Object> valueMap = new HashMap<>();
private final Map<String, FlagEvaluationConfig> evaluationConfigMap = new HashMap<>();
private static final Logger log = LoggerFactory.getLogger(ConfidenceStub.class);
private final List<String> callHistory = new ArrayList<>();

private ConfidenceStub() {}

public static ConfidenceStub createStub() {
return new ConfidenceStub();
}

@Override
protected ClientDelegate client() {
return new MockClientDelegate();
}

@Override
public <T> 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 <T> FlagEvaluation<T> 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 <T> 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<String> 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<ConfidenceValue.Struct> message) {
// No-op
}

@Override
public void flush() {
// No-op
}

@Override
public CompletableFuture<ResolveFlagsResponse> 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
}