DummyConnection.java
/*
* SPDX-FileCopyrightText: 2025 Lucimber UG
* SPDX-License-Identifier: Apache-2.0
*/
package com.lucimber.dbus.connection;
import com.lucimber.dbus.message.InboundError;
import com.lucimber.dbus.message.InboundMessage;
import com.lucimber.dbus.message.InboundMethodReturn;
import com.lucimber.dbus.message.OutboundMessage;
import com.lucimber.dbus.message.OutboundMethodCall;
import com.lucimber.dbus.type.DBusSignature;
import com.lucimber.dbus.type.DBusString;
import com.lucimber.dbus.type.DBusType;
import com.lucimber.dbus.type.DBusUInt32;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* A dummy implementation of {@link Connection} for testing D-Bus applications without requiring a
* real D-Bus daemon, similar to Netty's {@code EmbeddedChannel}.
*
* <p>This class provides a complete implementation of the {@link Connection} interface that can be
* used in unit tests and integration tests. It simulates D-Bus behavior including connection
* lifecycle, message handling, and error conditions.
*
* <h2>Key Features</h2>
*
* <ul>
* <li><strong>No D-Bus daemon required</strong> - Works entirely in-memory
* <li><strong>Configurable responses</strong> - Define custom responses for method calls
* <li><strong>Connection lifecycle simulation</strong> - Realistic state transitions
* <li><strong>Message capture</strong> - Inspect sent messages for verification
* <li><strong>Error simulation</strong> - Test connection failures and recovery
* <li><strong>Thread-safe</strong> - Safe for concurrent testing
* </ul>
*
* <h2>Basic Usage</h2>
*
* <pre>{@code
* @Test
* public void testMyService() {
* // Create a dummy connection
* DummyConnection connection = DummyConnection.create();
*
* // Define a custom response
* connection.setMethodCallResponse("com.example.Service", "GetData",
* DummyConnection.successResponse(List.of(DBusString.valueOf("test-data"))));
*
* // Connect and use in your service
* connection.connect().toCompletableFuture().get();
* MyService service = new MyService(connection);
*
* // Test the service
* String result = service.getData();
* assertEquals("test-data", result);
*
* // Verify the method was called
* assertTrue(connection.wasMethodCalled("com.example.Service", "GetData"));
* }
* }</pre>
*
* <h2>Advanced Testing</h2>
*
* <pre>{@code
* @Test
* public void testConnectionFailure() {
* DummyConnection connection = DummyConnection.builder()
* .withConnectionFailure(true)
* .build();
*
* // Test how your code handles connection failures
* assertThrows(Exception.class, () -> connection.connect().toCompletableFuture().get());
* }
*
* @Test
* public void testMessageCapture() {
* DummyConnection connection = DummyConnection.create();
* connection.connect().toCompletableFuture().get();
*
* // Your code sends messages
* myService.performAction();
*
* // Verify messages were sent
* List<OutboundMessage> messages = connection.getSentMessages();
* assertEquals(1, messages.size());
*
* OutboundMethodCall call = (OutboundMethodCall) messages.get(0);
* assertEquals("PerformAction", call.getMember().toString());
* }
* }</pre>
*
* <h2>Error Testing</h2>
*
* <pre>{@code
* @Test
* public void testErrorHandling() {
* DummyConnection connection = DummyConnection.create();
* connection.setMethodCallResponse("com.example.Service", "FailingMethod",
* DummyConnection.errorResponse("com.example.Error", "Something went wrong"));
*
* connection.connect().toCompletableFuture().get();
*
* // Test that your code handles D-Bus errors properly
* assertThrows(MyServiceException.class, () -> myService.callFailingMethod());
* }
* }</pre>
*
* <h2>Connection Events</h2>
*
* <pre>{@code
* @Test
* public void testConnectionEvents() {
* DummyConnection connection = DummyConnection.create();
* AtomicBoolean connected = new AtomicBoolean(false);
*
* connection.addConnectionEventListener((conn, event) -> {
* if (event.getType() == ConnectionEventType.STATE_CHANGED
* && event.getNewState().orElse(null) == ConnectionState.CONNECTED) {
* connected.set(true);
* }
* });
*
* connection.connect().toCompletableFuture().get();
* assertTrue(connected.get());
* }
* }</pre>
*
* <h2>Thread Safety</h2>
*
* <p>This class is thread-safe and can be used in concurrent tests. All methods can be called from
* multiple threads without external synchronization.
*
* <h2>Cleanup</h2>
*
* <p>Always call {@link #close()} when done testing to clean up resources:
*
* <pre>{@code
* @Test
* public void testWithCleanup() {
* DummyConnection connection = DummyConnection.create();
* try {
* // Your test code
* } finally {
* connection.close();
* }
* }
* }</pre>
*
* @since 2.0
*/
public class DummyConnection implements Connection {
private final AtomicInteger serialCounter = new AtomicInteger(1);
private final AtomicReference<ConnectionState> state =
new AtomicReference<>(ConnectionState.DISCONNECTED);
private final AtomicInteger reconnectAttempts = new AtomicInteger(0);
private final DummyPipeline pipeline;
private final ConnectionConfig config;
private final CopyOnWriteArrayList<ConnectionEventListener> listeners =
new CopyOnWriteArrayList<>();
private final Map<String, Function<OutboundMessage, InboundMessage>> responseHandlers =
new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(
2,
r -> {
Thread t = new Thread(r, "DummyConnection-Scheduler");
t.setDaemon(true);
return t;
});
// Message capture for testing
private final BlockingQueue<OutboundMessage> sentMessages = new LinkedBlockingQueue<>();
private final BlockingQueue<ConnectionEvent> connectionEvents = new LinkedBlockingQueue<>();
// Test configuration
private final Duration connectDelay;
private final boolean shouldFailConnection;
private final boolean shouldFailHealthCheck;
private volatile boolean closed = false;
private volatile CompletableFuture<Void> currentHealthCheck;
private DummyConnection(Builder builder) {
this.config = builder.config;
this.connectDelay = builder.connectDelay;
this.shouldFailConnection = builder.shouldFailConnection;
this.shouldFailHealthCheck = builder.shouldFailHealthCheck;
this.pipeline = new DummyPipeline(this);
// Set up response handlers
this.responseHandlers.putAll(builder.responseHandlers);
setupDefaultResponses();
}
/**
* Creates a new builder for configuring a DummyConnection.
*
* @return a new Builder instance
*/
public static Builder builder() {
return new Builder();
}
/**
* Creates a simple DummyConnection with default configuration.
*
* @return a new DummyConnection instance ready for use
*/
public static DummyConnection create() {
return new Builder().build();
}
// Connection interface implementation
@Override
public CompletionStage<Void> connect() {
if (closed) {
var e = new IllegalStateException("Connection is closed");
return CompletableFuture.failedFuture(e);
}
if (state.compareAndSet(ConnectionState.DISCONNECTED, ConnectionState.CONNECTING)) {
CompletableFuture<Void> connectFuture = new CompletableFuture<>();
// Fire connecting event
fireConnectionEvent(
ConnectionEvent.stateChanged(
ConnectionState.DISCONNECTED, ConnectionState.CONNECTING));
scheduler.schedule(
() -> {
try {
if (shouldFailConnection) {
ConnectionState oldState = state.getAndSet(ConnectionState.FAILED);
var e = new RuntimeException("Simulated connection failure");
connectFuture.completeExceptionally(e);
fireConnectionEvent(
ConnectionEvent.stateChanged(
oldState, ConnectionState.FAILED));
} else {
// Transition to authenticating
state.set(ConnectionState.AUTHENTICATING);
fireConnectionEvent(
ConnectionEvent.stateChanged(
ConnectionState.CONNECTING,
ConnectionState.AUTHENTICATING));
// Simulate authentication delay
scheduler.schedule(
() -> {
ConnectionState oldState =
state.getAndSet(ConnectionState.CONNECTED);
connectFuture.complete(null);
fireConnectionEvent(
ConnectionEvent.stateChanged(
oldState, ConnectionState.CONNECTED));
},
connectDelay.toMillis() / 2,
TimeUnit.MILLISECONDS);
}
} catch (Exception e) {
ConnectionState oldState = state.getAndSet(ConnectionState.FAILED);
connectFuture.completeExceptionally(e);
fireConnectionEvent(
ConnectionEvent.stateChanged(oldState, ConnectionState.FAILED));
}
},
connectDelay.toMillis(),
TimeUnit.MILLISECONDS);
return connectFuture;
}
return CompletableFuture.completedFuture(null);
}
@Override
public boolean isConnected() {
return state.get() == ConnectionState.CONNECTED;
}
@Override
public Pipeline getPipeline() {
return pipeline;
}
@Override
public DBusUInt32 getNextSerial() {
return DBusUInt32.valueOf(serialCounter.getAndIncrement());
}
@Override
public CompletionStage<InboundMessage> sendRequest(OutboundMessage msg) {
if (!state.get().canHandleRequests()) {
var e = new IllegalStateException("Connection not ready: " + state.get());
return CompletableFuture.failedFuture(e);
}
// Capture the message for testing
sentMessages.offer(msg);
return CompletableFuture.supplyAsync(
() -> {
// Simulate network delay
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted", e);
}
return generateResponse(msg);
},
scheduler);
}
@Override
public void sendAndRouteResponse(OutboundMessage msg, CompletionStage<Void> future) {
if (!state.get().canHandleRequests()) {
var e = new IllegalStateException("Connection not ready: " + state.get());
future.toCompletableFuture().completeExceptionally(e);
return;
}
// Capture the message for testing
sentMessages.offer(msg);
// Simulate sending the message
scheduler.schedule(
() -> {
try {
future.toCompletableFuture().complete(null);
} catch (Exception e) {
future.toCompletableFuture().completeExceptionally(e);
}
},
5,
TimeUnit.MILLISECONDS);
}
@Override
public ConnectionConfig getConfig() {
return config;
}
@Override
public ConnectionState getState() {
return state.get();
}
@Override
public void addConnectionEventListener(ConnectionEventListener listener) {
listeners.add(Objects.requireNonNull(listener));
}
@Override
public void removeConnectionEventListener(ConnectionEventListener listener) {
listeners.remove(listener);
}
@Override
public CompletionStage<Void> triggerHealthCheck() {
if (currentHealthCheck != null && !currentHealthCheck.isDone()) {
return currentHealthCheck;
}
currentHealthCheck = new CompletableFuture<>();
scheduler.schedule(
() -> {
if (shouldFailHealthCheck) {
ConnectionState oldState = state.getAndSet(ConnectionState.UNHEALTHY);
currentHealthCheck.completeExceptionally(
new RuntimeException("Health check failed"));
fireConnectionEvent(
ConnectionEvent.healthCheckFailure(
new RuntimeException("Simulated health check failure")));
if (oldState != ConnectionState.UNHEALTHY) {
fireConnectionEvent(
ConnectionEvent.stateChanged(
oldState, ConnectionState.UNHEALTHY));
}
} else {
ConnectionState currentState = state.get();
if (currentState == ConnectionState.UNHEALTHY) {
state.set(ConnectionState.CONNECTED);
fireConnectionEvent(
ConnectionEvent.stateChanged(
ConnectionState.UNHEALTHY, ConnectionState.CONNECTED));
}
currentHealthCheck.complete(null);
fireConnectionEvent(ConnectionEvent.healthCheckSuccess());
}
},
10,
TimeUnit.MILLISECONDS);
return currentHealthCheck;
}
@Override
public int getReconnectAttemptCount() {
return reconnectAttempts.get();
}
@Override
public void cancelReconnection() {
// For testing, we just simulate the event
}
@Override
public void resetReconnectionState() {
reconnectAttempts.set(0);
}
@Override
public void close() {
if (closed) {
return;
}
closed = true;
ConnectionState oldState = state.getAndSet(ConnectionState.DISCONNECTED);
if (oldState != ConnectionState.DISCONNECTED) {
fireConnectionEvent(
ConnectionEvent.stateChanged(oldState, ConnectionState.DISCONNECTED));
}
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
// Testing utility methods
/**
* Sets a response handler for method calls to a specific interface and method.
*
* @param interfaceName the D-Bus interface name
* @param methodName the method name
* @param responseFunction function to generate the response
*/
public final void setMethodCallResponse(
String interfaceName,
String methodName,
Function<OutboundMessage, InboundMessage> responseFunction) {
responseHandlers.put(interfaceName + "." + methodName, responseFunction);
}
/**
* Checks if a method call was made to the specified interface and method.
*
* @param interfaceName the D-Bus interface name
* @param methodName the method name
* @return true if the method was called
*/
public boolean wasMethodCalled(String interfaceName, String methodName) {
return sentMessages.stream()
.filter(OutboundMethodCall.class::isInstance)
.map(OutboundMethodCall.class::cast)
.anyMatch(
call ->
call.getInterfaceName()
.map(DBusString::toString)
.orElse("")
.equals(interfaceName)
&& call.getMember().toString().equals(methodName));
}
/**
* Returns the number of times a method was called.
*
* @param interfaceName the D-Bus interface name
* @param methodName the method name
* @return the number of times the method was called
*/
public int getMethodCallCount(String interfaceName, String methodName) {
return (int)
sentMessages.stream()
.filter(OutboundMethodCall.class::isInstance)
.map(OutboundMethodCall.class::cast)
.filter(
call ->
call.getInterfaceName()
.map(DBusString::toString)
.orElse("")
.equals(interfaceName)
&& call.getMember().toString().equals(methodName))
.count();
}
/**
* Returns all messages sent through this connection.
*
* @return a list of sent messages (safe to modify)
*/
public List<OutboundMessage> getSentMessages() {
return new ArrayList<>(sentMessages);
}
/**
* Returns all messages sent through this connection that match the given predicate.
*
* @param predicate the predicate to filter messages
* @return a list of matching messages
*/
public List<OutboundMessage> getSentMessages(Predicate<OutboundMessage> predicate) {
return sentMessages.stream().filter(predicate).toList();
}
/**
* Returns all method calls sent to a specific interface.
*
* @param interfaceName the D-Bus interface name
* @return a list of method calls to the interface
*/
public List<OutboundMethodCall> getMethodCalls(String interfaceName) {
return sentMessages.stream()
.filter(OutboundMethodCall.class::isInstance)
.map(OutboundMethodCall.class::cast)
.filter(
call ->
call.getInterfaceName()
.map(DBusString::toString)
.orElse("")
.equals(interfaceName))
.toList();
}
/**
* Returns all connection events that have occurred.
*
* @return a list of connection events (safe to modify)
*/
public List<ConnectionEvent> getConnectionEvents() {
return new ArrayList<>(connectionEvents);
}
/**
* Waits for a specific connection event to occur.
*
* @param eventType the type of event to wait for
* @param timeout the maximum time to wait
* @param unit the time unit of the timeout argument
* @return true if the event occurred, false if timeout
* @throws InterruptedException if the current thread is interrupted
*/
public boolean waitForEvent(ConnectionEventType eventType, long timeout, TimeUnit unit)
throws InterruptedException {
long endTime = System.nanoTime() + unit.toNanos(timeout);
while (System.nanoTime() < endTime) {
ConnectionEvent event = connectionEvents.poll(100, TimeUnit.MILLISECONDS);
if (event != null && event.getType() == eventType) {
return true;
}
}
return false;
}
/** Clears all captured messages and events. */
public void clearCaptures() {
sentMessages.clear();
connectionEvents.clear();
}
/** Simulates a connection failure by transitioning to FAILED state. */
public void simulateConnectionFailure() {
ConnectionState oldState = state.getAndSet(ConnectionState.FAILED);
if (oldState.canHandleRequests()) {
fireConnectionEvent(ConnectionEvent.stateChanged(oldState, ConnectionState.FAILED));
}
}
/** Simulates a reconnection attempt. */
public void simulateReconnection() {
if (state.compareAndSet(ConnectionState.FAILED, ConnectionState.RECONNECTING)) {
int attempts = reconnectAttempts.incrementAndGet();
fireConnectionEvent(ConnectionEvent.reconnectionAttempt(attempts));
scheduler.schedule(
() -> {
ConnectionState oldState = state.getAndSet(ConnectionState.CONNECTED);
fireConnectionEvent(
ConnectionEvent.stateChanged(oldState, ConnectionState.CONNECTED));
},
connectDelay.toMillis(),
TimeUnit.MILLISECONDS);
}
}
/**
* Creates a success response with the given body.
*
* @param body the response body
* @return a function that creates a success response
*/
public static Function<OutboundMessage, InboundMessage> successResponse(List<DBusType> body) {
return msg -> {
if (msg instanceof OutboundMethodCall call) {
var builder =
InboundMethodReturn.Builder.create()
.withSerial(
DBusUInt32.valueOf(
(int) (System.nanoTime() % Integer.MAX_VALUE)))
.withReplySerial(call.getSerial())
.withSender(DBusString.valueOf(":1.0"));
if (body != null && !body.isEmpty()) {
// Create signature based on body types
StringBuilder sigBuilder = new StringBuilder();
for (DBusType item : body) {
if (item instanceof DBusString) {
sigBuilder.append("s");
} else if (item instanceof DBusUInt32) {
sigBuilder.append("u");
} else {
sigBuilder.append("v"); // variant for unknown types
}
}
builder.withBody(DBusSignature.valueOf(sigBuilder.toString()), body);
}
// For empty bodies, don't call withBody at all - both signature and payload will be
// null
return builder.build();
}
throw new IllegalArgumentException("Unsupported message type: " + msg.getClass());
};
}
/**
* Creates an error response with the given error name and message.
*
* @param errorName the D-Bus error name
* @param errorMessage the error message
* @return a function that creates an error response
*/
public static Function<OutboundMessage, InboundMessage> errorResponse(
String errorName, String errorMessage) {
return msg -> {
if (msg instanceof OutboundMethodCall call) {
return InboundError.Builder.create()
.withSerial(
DBusUInt32.valueOf((int) (System.nanoTime() % Integer.MAX_VALUE)))
.withReplySerial(call.getSerial())
.withSender(DBusString.valueOf(":1.0"))
.withErrorName(DBusString.valueOf(errorName))
.withBody(
DBusSignature.valueOf("s"),
List.of(DBusString.valueOf(errorMessage)))
.build();
}
throw new IllegalArgumentException("Unsupported message type: " + msg.getClass());
};
}
private void setupDefaultResponses() {
// Default response for D-Bus introspection
setMethodCallResponse(
"org.freedesktop.DBus.Introspectable",
"Introspect",
successResponse(
List.of(
DBusString.valueOf(
"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD"
+ " D-BUS Object Introspection 1.0//EN\""
+ " \"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\"><node></node>"))));
// Default response for D-Bus Peer interface
setMethodCallResponse("org.freedesktop.DBus.Peer", "Ping", successResponse(List.of()));
setMethodCallResponse(
"org.freedesktop.DBus.Peer",
"GetMachineId",
successResponse(
List.of(DBusString.valueOf("dummy-machine-id-" + System.nanoTime()))));
}
private InboundMessage generateResponse(OutboundMessage msg) {
if (msg instanceof OutboundMethodCall call) {
String interfaceName =
call.getInterfaceName().map(DBusString::toString).orElse("unknown");
String methodName = call.getMember().toString();
String key = interfaceName + "." + methodName;
Function<OutboundMessage, InboundMessage> handler = responseHandlers.get(key);
if (handler != null) {
return handler.apply(msg);
}
// Default error response for unknown methods
return errorResponse(
"org.freedesktop.DBus.Error.UnknownMethod",
"Unknown method: " + methodName + " on interface: " + interfaceName)
.apply(msg);
}
throw new IllegalArgumentException("Unsupported message type: " + msg.getClass());
}
private void fireConnectionEvent(ConnectionEvent event) {
connectionEvents.offer(event);
for (ConnectionEventListener listener : listeners) {
try {
listener.onConnectionEvent(this, event);
} catch (Exception e) {
// Log and continue with other listeners
System.err.println("Error in connection event listener: " + e.getMessage());
}
}
}
/** Simple pipeline implementation for testing purposes. */
private static class DummyPipeline implements Pipeline {
private final Connection connection;
private final CopyOnWriteArrayList<HandlerEntry> handlers = new CopyOnWriteArrayList<>();
private final Map<String, HandlerEntry> handlerMap = new ConcurrentHashMap<>();
private DummyPipeline(Connection connection) {
this.connection = connection;
}
@Override
public Pipeline addLast(String name, Handler handler) {
Objects.requireNonNull(name, "Handler name cannot be null");
Objects.requireNonNull(handler, "Handler cannot be null");
HandlerEntry entry = new HandlerEntry(name, handler);
if (handlerMap.putIfAbsent(name, entry) != null) {
throw new IllegalArgumentException(
"Handler with name '" + name + "' already exists");
}
handlers.add(entry);
return this;
}
@Override
public Connection getConnection() {
return connection;
}
@Override
public Pipeline remove(String name) {
Objects.requireNonNull(name, "Handler name cannot be null");
HandlerEntry entry = handlerMap.remove(name);
if (entry == null) {
throw new IllegalArgumentException("No handler with name '" + name + "' exists");
}
handlers.remove(entry);
return this;
}
@Override
public void propagateInboundMessage(InboundMessage msg) {
// For testing, we don't need complex propagation
}
@Override
public void propagateOutboundMessage(OutboundMessage msg, CompletableFuture<Void> future) {
// For testing, we don't need complex propagation
future.complete(null);
}
@Override
public void propagateConnectionActive() {
// For testing, we don't need complex propagation
}
@Override
public void propagateConnectionInactive() {
// For testing, we don't need complex propagation
}
@Override
public void propagateInboundFailure(Throwable cause) {
// For testing, we don't need complex propagation
}
private record HandlerEntry(String name, Handler handler) {
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
HandlerEntry that = (HandlerEntry) obj;
return Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
}
/** Builder for creating DummyConnection instances with custom configuration. */
public static class Builder {
private ConnectionConfig config = ConnectionConfig.builder().build();
private Duration connectDelay = Duration.ofMillis(50);
private boolean shouldFailConnection = false;
private boolean shouldFailHealthCheck = false;
private final Map<String, Function<OutboundMessage, InboundMessage>> responseHandlers =
new ConcurrentHashMap<>();
/**
* Sets the connection configuration.
*
* @param config the connection configuration
* @return this builder
*/
public Builder withConfig(ConnectionConfig config) {
this.config = Objects.requireNonNull(config);
return this;
}
/**
* Sets the simulated connection delay.
*
* @param delay the connection delay
* @return this builder
*/
public Builder withConnectDelay(Duration delay) {
this.connectDelay = Objects.requireNonNull(delay);
return this;
}
/**
* Configures the connection to fail during connect.
*
* @param shouldFail true to simulate connection failure
* @return this builder
*/
public Builder withConnectionFailure(boolean shouldFail) {
this.shouldFailConnection = shouldFail;
return this;
}
/**
* Configures health checks to fail.
*
* @param shouldFail true to simulate health check failure
* @return this builder
*/
public Builder withHealthCheckFailure(boolean shouldFail) {
this.shouldFailHealthCheck = shouldFail;
return this;
}
/**
* Adds a response handler for method calls.
*
* @param interfaceName the D-Bus interface name
* @param methodName the method name
* @param responseFunction function to generate the response
* @return this builder
*/
public Builder withMethodCallResponse(
String interfaceName,
String methodName,
Function<OutboundMessage, InboundMessage> responseFunction) {
responseHandlers.put(interfaceName + "." + methodName, responseFunction);
return this;
}
/**
* Builds the DummyConnection instance.
*
* @return a new DummyConnection
*/
public DummyConnection build() {
return new DummyConnection(this);
}
}
}