From a65cf3473d5d24819dcb59e22dd0f7e81dab23d2 Mon Sep 17 00:00:00 2001
From: bbaker <bbaker@atlassian.com>
Date: Thu, 30 Jan 2025 13:20:19 +1100
Subject: [PATCH] Making DataLoaderOptions immutable

---
 .../org/dataloader/DataLoaderOptions.java     | 240 ++++++++++++++----
 .../org/dataloader/DataLoaderOptionsTest.java | 187 ++++++++++++++
 .../org/dataloader/ValueCacheOptionsTest.java |  19 ++
 3 files changed, 396 insertions(+), 50 deletions(-)
 create mode 100644 src/test/java/org/dataloader/DataLoaderOptionsTest.java
 create mode 100644 src/test/java/org/dataloader/ValueCacheOptionsTest.java

diff --git a/src/main/java/org/dataloader/DataLoaderOptions.java b/src/main/java/org/dataloader/DataLoaderOptions.java
index b96e785..f8ea95c 100644
--- a/src/main/java/org/dataloader/DataLoaderOptions.java
+++ b/src/main/java/org/dataloader/DataLoaderOptions.java
@@ -17,18 +17,20 @@
 package org.dataloader;
 
 import org.dataloader.annotations.PublicApi;
-import org.dataloader.impl.Assertions;
 import org.dataloader.scheduler.BatchLoaderScheduler;
 import org.dataloader.stats.NoOpStatisticsCollector;
 import org.dataloader.stats.StatisticsCollector;
 
+import java.util.Objects;
 import java.util.Optional;
+import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 import static org.dataloader.impl.Assertions.nonNull;
 
 /**
- * Configuration options for {@link DataLoader} instances.
+ * Configuration options for {@link DataLoader} instances.  This is an immutable class so each time
+ * you change a value it returns a new object.
  *
  * @author <a href="https://github.com/aschrijver/">Arnold Schrijver</a>
  */
@@ -36,18 +38,20 @@
 public class DataLoaderOptions {
 
     private static final BatchLoaderContextProvider NULL_PROVIDER = () -> null;
-
-    private boolean batchingEnabled;
-    private boolean cachingEnabled;
-    private boolean cachingExceptionsEnabled;
-    private CacheKey<?> cacheKeyFunction;
-    private CacheMap<?, ?> cacheMap;
-    private ValueCache<?, ?> valueCache;
-    private int maxBatchSize;
-    private Supplier<StatisticsCollector> statisticsCollector;
-    private BatchLoaderContextProvider environmentProvider;
-    private ValueCacheOptions valueCacheOptions;
-    private BatchLoaderScheduler batchLoaderScheduler;
+    private static final Supplier<StatisticsCollector> NOOP_COLLECTOR = NoOpStatisticsCollector::new;
+    private static final ValueCacheOptions DEFAULT_VALUE_CACHE_OPTIONS = ValueCacheOptions.newOptions();
+
+    private final boolean batchingEnabled;
+    private final boolean cachingEnabled;
+    private final boolean cachingExceptionsEnabled;
+    private final CacheKey<?> cacheKeyFunction;
+    private final CacheMap<?, ?> cacheMap;
+    private final ValueCache<?, ?> valueCache;
+    private final int maxBatchSize;
+    private final Supplier<StatisticsCollector> statisticsCollector;
+    private final BatchLoaderContextProvider environmentProvider;
+    private final ValueCacheOptions valueCacheOptions;
+    private final BatchLoaderScheduler batchLoaderScheduler;
 
     /**
      * Creates a new data loader options with default settings.
@@ -56,13 +60,30 @@ public DataLoaderOptions() {
         batchingEnabled = true;
         cachingEnabled = true;
         cachingExceptionsEnabled = true;
+        cacheKeyFunction = null;
+        cacheMap = null;
+        valueCache = null;
         maxBatchSize = -1;
-        statisticsCollector = NoOpStatisticsCollector::new;
+        statisticsCollector = NOOP_COLLECTOR;
         environmentProvider = NULL_PROVIDER;
-        valueCacheOptions = ValueCacheOptions.newOptions();
+        valueCacheOptions = DEFAULT_VALUE_CACHE_OPTIONS;
         batchLoaderScheduler = null;
     }
 
+    private DataLoaderOptions(Builder builder) {
+        this.batchingEnabled = builder.batchingEnabled;
+        this.cachingEnabled = builder.cachingEnabled;
+        this.cachingExceptionsEnabled = builder.cachingExceptionsEnabled;
+        this.cacheKeyFunction = builder.cacheKeyFunction;
+        this.cacheMap = builder.cacheMap;
+        this.valueCache = builder.valueCache;
+        this.maxBatchSize = builder.maxBatchSize;
+        this.statisticsCollector = builder.statisticsCollector;
+        this.environmentProvider = builder.environmentProvider;
+        this.valueCacheOptions = builder.valueCacheOptions;
+        this.batchLoaderScheduler = builder.batchLoaderScheduler;
+    }
+
     /**
      * Clones the provided data loader options.
      *
@@ -90,6 +111,51 @@ public static DataLoaderOptions newOptions() {
         return new DataLoaderOptions();
     }
 
+    /**
+     * @return a new default data loader options {@link Builder} that you can then customize
+     */
+    public static DataLoaderOptions.Builder newOptionsBuilder() {
+        return new DataLoaderOptions.Builder();
+    }
+
+    /**
+     * @param otherOptions the options to copy
+     * @return a new default data loader options {@link Builder} from the specified one that you can then customize
+     */
+    public static DataLoaderOptions.Builder newDataLoaderOptions(DataLoaderOptions otherOptions) {
+        return new DataLoaderOptions.Builder(otherOptions);
+    }
+
+    /**
+     * Will transform the current options in to a builder ands allow you to build a new set of options
+     *
+     * @param builderConsumer the consumer of a builder that has this objects starting values
+     * @return a new {@link DataLoaderOptions} object
+     */
+    public DataLoaderOptions transform(Consumer<Builder> builderConsumer) {
+        Builder builder = newOptionsBuilder();
+        builderConsumer.accept(builder);
+        return builder.build();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null || getClass() != o.getClass()) return false;
+        DataLoaderOptions that = (DataLoaderOptions) o;
+        return batchingEnabled == that.batchingEnabled
+                && cachingEnabled == that.cachingEnabled
+                && cachingExceptionsEnabled == that.cachingExceptionsEnabled
+                && maxBatchSize == that.maxBatchSize
+                && Objects.equals(cacheKeyFunction, that.cacheKeyFunction) &&
+                Objects.equals(cacheMap, that.cacheMap) &&
+                Objects.equals(valueCache, that.valueCache) &&
+                Objects.equals(statisticsCollector, that.statisticsCollector) &&
+                Objects.equals(environmentProvider, that.environmentProvider) &&
+                Objects.equals(valueCacheOptions, that.valueCacheOptions) &&
+                Objects.equals(batchLoaderScheduler, that.batchLoaderScheduler);
+    }
+
+
     /**
      * Option that determines whether to use batching (the default), or not.
      *
@@ -103,12 +169,10 @@ public boolean batchingEnabled() {
      * Sets the option that determines whether batch loading is enabled.
      *
      * @param batchingEnabled {@code true} to enable batch loading, {@code false} otherwise
-     *
      * @return the data loader options for fluent coding
      */
     public DataLoaderOptions setBatchingEnabled(boolean batchingEnabled) {
-        this.batchingEnabled = batchingEnabled;
-        return this;
+        return builder().setBatchingEnabled(batchingEnabled).build();
     }
 
     /**
@@ -124,17 +188,15 @@ public boolean cachingEnabled() {
      * Sets the option that determines whether caching is enabled.
      *
      * @param cachingEnabled {@code true} to enable caching, {@code false} otherwise
-     *
      * @return the data loader options for fluent coding
      */
     public DataLoaderOptions setCachingEnabled(boolean cachingEnabled) {
-        this.cachingEnabled = cachingEnabled;
-        return this;
+        return builder().setCachingEnabled(cachingEnabled).build();
     }
 
     /**
      * Option that determines whether to cache exceptional values (the default), or not.
-     *
+     * <p>
      * For short-lived caches (that is request caches) it makes sense to cache exceptions since
      * it's likely the key is still poisoned.  However, if you have long-lived caches, then it may make
      * sense to set this to false since the downstream system may have recovered from its failure
@@ -150,12 +212,10 @@ public boolean cachingExceptionsEnabled() {
      * Sets the option that determines whether exceptional values are cache enabled.
      *
      * @param cachingExceptionsEnabled {@code true} to enable caching exceptional values, {@code false} otherwise
-     *
      * @return the data loader options for fluent coding
      */
     public DataLoaderOptions setCachingExceptionsEnabled(boolean cachingExceptionsEnabled) {
-        this.cachingExceptionsEnabled = cachingExceptionsEnabled;
-        return this;
+        return builder().setCachingExceptionsEnabled(cachingExceptionsEnabled).build();
     }
 
     /**
@@ -173,12 +233,10 @@ public Optional<CacheKey> cacheKeyFunction() {
      * Sets the function to use for creating the cache key, if caching is enabled.
      *
      * @param cacheKeyFunction the cache key function to use
-     *
      * @return the data loader options for fluent coding
      */
     public DataLoaderOptions setCacheKeyFunction(CacheKey<?> cacheKeyFunction) {
-        this.cacheKeyFunction = cacheKeyFunction;
-        return this;
+        return builder().setCacheKeyFunction(cacheKeyFunction).build();
     }
 
     /**
@@ -196,12 +254,10 @@ public DataLoaderOptions setCacheKeyFunction(CacheKey<?> cacheKeyFunction) {
      * Sets the cache map implementation to use for caching, if caching is enabled.
      *
      * @param cacheMap the cache map instance
-     *
      * @return the data loader options for fluent coding
      */
     public DataLoaderOptions setCacheMap(CacheMap<?, ?> cacheMap) {
-        this.cacheMap = cacheMap;
-        return this;
+        return builder().setCacheMap(cacheMap).build();
     }
 
     /**
@@ -219,12 +275,10 @@ public int maxBatchSize() {
      * before they are split into multiple class
      *
      * @param maxBatchSize the maximum batch size
-     *
      * @return the data loader options for fluent coding
      */
     public DataLoaderOptions setMaxBatchSize(int maxBatchSize) {
-        this.maxBatchSize = maxBatchSize;
-        return this;
+        return builder().setMaxBatchSize(maxBatchSize).build();
     }
 
     /**
@@ -240,12 +294,10 @@ public StatisticsCollector getStatisticsCollector() {
      * a common value
      *
      * @param statisticsCollector the statistics collector to use
-     *
      * @return the data loader options for fluent coding
      */
     public DataLoaderOptions setStatisticsCollector(Supplier<StatisticsCollector> statisticsCollector) {
-        this.statisticsCollector = nonNull(statisticsCollector);
-        return this;
+        return builder().setStatisticsCollector(nonNull(statisticsCollector)).build();
     }
 
     /**
@@ -259,12 +311,10 @@ public BatchLoaderContextProvider getBatchLoaderContextProvider() {
      * Sets the batch loader environment provider that will be used to give context to batch load functions
      *
      * @param contextProvider the batch loader context provider
-     *
      * @return the data loader options for fluent coding
      */
     public DataLoaderOptions setBatchLoaderContextProvider(BatchLoaderContextProvider contextProvider) {
-        this.environmentProvider = nonNull(contextProvider);
-        return this;
+        return builder().setBatchLoaderContextProvider(nonNull(contextProvider)).build();
     }
 
     /**
@@ -282,12 +332,10 @@ public DataLoaderOptions setBatchLoaderContextProvider(BatchLoaderContextProvide
      * Sets the value cache implementation to use for caching values, if caching is enabled.
      *
      * @param valueCache the value cache instance
-     *
      * @return the data loader options for fluent coding
      */
     public DataLoaderOptions setValueCache(ValueCache<?, ?> valueCache) {
-        this.valueCache = valueCache;
-        return this;
+        return builder().setValueCache(valueCache).build();
     }
 
     /**
@@ -301,12 +349,10 @@ public ValueCacheOptions getValueCacheOptions() {
      * Sets the {@link ValueCacheOptions} that control how the {@link ValueCache} will be used
      *
      * @param valueCacheOptions the value cache options
-     *
      * @return the data loader options for fluent coding
      */
     public DataLoaderOptions setValueCacheOptions(ValueCacheOptions valueCacheOptions) {
-        this.valueCacheOptions = Assertions.nonNull(valueCacheOptions);
-        return this;
+        return builder().setValueCacheOptions(nonNull(valueCacheOptions)).build();
     }
 
     /**
@@ -321,11 +367,105 @@ public BatchLoaderScheduler getBatchLoaderScheduler() {
      * to some future time.
      *
      * @param batchLoaderScheduler the scheduler
-     *
      * @return the data loader options for fluent coding
      */
     public DataLoaderOptions setBatchLoaderScheduler(BatchLoaderScheduler batchLoaderScheduler) {
-        this.batchLoaderScheduler = batchLoaderScheduler;
-        return this;
+        return builder().setBatchLoaderScheduler(batchLoaderScheduler).build();
+    }
+
+    private Builder builder() {
+        return new Builder(this);
+    }
+
+    public static class Builder {
+        private boolean batchingEnabled;
+        private boolean cachingEnabled;
+        private boolean cachingExceptionsEnabled;
+        private CacheKey<?> cacheKeyFunction;
+        private CacheMap<?, ?> cacheMap;
+        private ValueCache<?, ?> valueCache;
+        private int maxBatchSize;
+        private Supplier<StatisticsCollector> statisticsCollector;
+        private BatchLoaderContextProvider environmentProvider;
+        private ValueCacheOptions valueCacheOptions;
+        private BatchLoaderScheduler batchLoaderScheduler;
+
+        public Builder() {
+            this(new DataLoaderOptions()); // use the defaults of the DataLoaderOptions for this builder
+        }
+
+        Builder(DataLoaderOptions other) {
+            this.batchingEnabled = other.batchingEnabled;
+            this.cachingEnabled = other.cachingEnabled;
+            this.cachingExceptionsEnabled = other.cachingExceptionsEnabled;
+            this.cacheKeyFunction = other.cacheKeyFunction;
+            this.cacheMap = other.cacheMap;
+            this.valueCache = other.valueCache;
+            this.maxBatchSize = other.maxBatchSize;
+            this.statisticsCollector = other.statisticsCollector;
+            this.environmentProvider = other.environmentProvider;
+            this.valueCacheOptions = other.valueCacheOptions;
+            this.batchLoaderScheduler = other.batchLoaderScheduler;
+        }
+
+        public Builder setBatchingEnabled(boolean batchingEnabled) {
+            this.batchingEnabled = batchingEnabled;
+            return this;
+        }
+
+        public Builder setCachingEnabled(boolean cachingEnabled) {
+            this.cachingEnabled = cachingEnabled;
+            return this;
+        }
+
+        public Builder setCachingExceptionsEnabled(boolean cachingExceptionsEnabled) {
+            this.cachingExceptionsEnabled = cachingExceptionsEnabled;
+            return this;
+        }
+
+        public Builder setCacheKeyFunction(CacheKey<?> cacheKeyFunction) {
+            this.cacheKeyFunction = cacheKeyFunction;
+            return this;
+        }
+
+        public Builder setCacheMap(CacheMap<?, ?> cacheMap) {
+            this.cacheMap = cacheMap;
+            return this;
+        }
+
+        public Builder setValueCache(ValueCache<?, ?> valueCache) {
+            this.valueCache = valueCache;
+            return this;
+        }
+
+        public Builder setMaxBatchSize(int maxBatchSize) {
+            this.maxBatchSize = maxBatchSize;
+            return this;
+        }
+
+        public Builder setStatisticsCollector(Supplier<StatisticsCollector> statisticsCollector) {
+            this.statisticsCollector = statisticsCollector;
+            return this;
+        }
+
+        public Builder setBatchLoaderContextProvider(BatchLoaderContextProvider environmentProvider) {
+            this.environmentProvider = environmentProvider;
+            return this;
+        }
+
+        public Builder setValueCacheOptions(ValueCacheOptions valueCacheOptions) {
+            this.valueCacheOptions = valueCacheOptions;
+            return this;
+        }
+
+        public Builder setBatchLoaderScheduler(BatchLoaderScheduler batchLoaderScheduler) {
+            this.batchLoaderScheduler = batchLoaderScheduler;
+            return this;
+        }
+
+        public DataLoaderOptions build() {
+            return new DataLoaderOptions(this);
+        }
+
     }
 }
diff --git a/src/test/java/org/dataloader/DataLoaderOptionsTest.java b/src/test/java/org/dataloader/DataLoaderOptionsTest.java
new file mode 100644
index 0000000..f6e06e8
--- /dev/null
+++ b/src/test/java/org/dataloader/DataLoaderOptionsTest.java
@@ -0,0 +1,187 @@
+package org.dataloader;
+
+import org.dataloader.impl.DefaultCacheMap;
+import org.dataloader.impl.NoOpValueCache;
+import org.dataloader.scheduler.BatchLoaderScheduler;
+import org.dataloader.stats.NoOpStatisticsCollector;
+import org.dataloader.stats.StatisticsCollector;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletionStage;
+import java.util.function.Supplier;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@SuppressWarnings("OptionalGetWithoutIsPresent")
+class DataLoaderOptionsTest {
+
+    DataLoaderOptions optionsDefault = new DataLoaderOptions();
+
+    @Test
+    void canCreateDefaultOptions() {
+
+        assertThat(optionsDefault.batchingEnabled(), equalTo(true));
+        assertThat(optionsDefault.cachingEnabled(), equalTo(true));
+        assertThat(optionsDefault.cachingExceptionsEnabled(), equalTo(true));
+        assertThat(optionsDefault.maxBatchSize(), equalTo(-1));
+        assertThat(optionsDefault.getBatchLoaderScheduler(), equalTo(null));
+
+        DataLoaderOptions builtOptions = DataLoaderOptions.newOptionsBuilder().build();
+        assertThat(builtOptions, equalTo(optionsDefault));
+        assertThat(builtOptions == optionsDefault, equalTo(false));
+
+        DataLoaderOptions transformedOptions = optionsDefault.transform(builder -> {
+        });
+        assertThat(transformedOptions, equalTo(optionsDefault));
+        assertThat(transformedOptions == optionsDefault, equalTo(false));
+    }
+
+    @Test
+    void canCopyOk() {
+        DataLoaderOptions optionsNext = new DataLoaderOptions(optionsDefault);
+        assertThat(optionsNext, equalTo(optionsDefault));
+        assertThat(optionsNext == optionsDefault, equalTo(false));
+
+        optionsNext = DataLoaderOptions.newDataLoaderOptions(optionsDefault).build();
+        assertThat(optionsNext, equalTo(optionsDefault));
+        assertThat(optionsNext == optionsDefault, equalTo(false));
+    }
+
+    BatchLoaderScheduler testBatchLoaderScheduler = new BatchLoaderScheduler() {
+        @Override
+        public <K, V> CompletionStage<List<V>> scheduleBatchLoader(ScheduledBatchLoaderCall<V> scheduledCall, List<K> keys, BatchLoaderEnvironment environment) {
+            return null;
+        }
+
+        @Override
+        public <K, V> CompletionStage<Map<K, V>> scheduleMappedBatchLoader(ScheduledMappedBatchLoaderCall<K, V> scheduledCall, List<K> keys, BatchLoaderEnvironment environment) {
+            return null;
+        }
+
+        @Override
+        public <K> void scheduleBatchPublisher(ScheduledBatchPublisherCall scheduledCall, List<K> keys, BatchLoaderEnvironment environment) {
+
+        }
+    };
+
+    BatchLoaderContextProvider testBatchLoaderContextProvider = () -> null;
+
+    CacheMap<Object, Object> testCacheMap = new DefaultCacheMap<>();
+
+    ValueCache<Object, Object> testValueCache = new NoOpValueCache<>();
+
+    CacheKey<Object> testCacheKey = new CacheKey<Object>() {
+        @Override
+        public Object getKey(Object input) {
+            return null;
+        }
+    };
+
+    ValueCacheOptions testValueCacheOptions = ValueCacheOptions.newOptions();
+
+    NoOpStatisticsCollector noOpStatisticsCollector = new NoOpStatisticsCollector();
+    Supplier<StatisticsCollector> testStatisticsCollectorSupplier = () -> noOpStatisticsCollector;
+
+    @Test
+    void canBuildOk() {
+        assertThat(optionsDefault.setBatchingEnabled(false).batchingEnabled(),
+                equalTo(false));
+        assertThat(optionsDefault.setBatchLoaderScheduler(testBatchLoaderScheduler).getBatchLoaderScheduler(),
+                equalTo(testBatchLoaderScheduler));
+        assertThat(optionsDefault.setBatchLoaderContextProvider(testBatchLoaderContextProvider).getBatchLoaderContextProvider(),
+                equalTo(testBatchLoaderContextProvider));
+        assertThat(optionsDefault.setCacheMap(testCacheMap).cacheMap().get(),
+                equalTo(testCacheMap));
+        assertThat(optionsDefault.setCachingEnabled(false).cachingEnabled(),
+                equalTo(false));
+        assertThat(optionsDefault.setValueCacheOptions(testValueCacheOptions).getValueCacheOptions(),
+                equalTo(testValueCacheOptions));
+        assertThat(optionsDefault.setCacheKeyFunction(testCacheKey).cacheKeyFunction().get(),
+                equalTo(testCacheKey));
+        assertThat(optionsDefault.setValueCache(testValueCache).valueCache().get(),
+                equalTo(testValueCache));
+        assertThat(optionsDefault.setMaxBatchSize(10).maxBatchSize(),
+                equalTo(10));
+        assertThat(optionsDefault.setStatisticsCollector(testStatisticsCollectorSupplier).getStatisticsCollector(),
+                equalTo(testStatisticsCollectorSupplier.get()));
+
+        DataLoaderOptions builtOptions = optionsDefault.transform(builder -> {
+            builder.setBatchingEnabled(false);
+            builder.setCachingExceptionsEnabled(false);
+            builder.setCachingEnabled(false);
+            builder.setBatchLoaderScheduler(testBatchLoaderScheduler);
+            builder.setBatchLoaderContextProvider(testBatchLoaderContextProvider);
+            builder.setCacheMap(testCacheMap);
+            builder.setValueCache(testValueCache);
+            builder.setCacheKeyFunction(testCacheKey);
+            builder.setValueCacheOptions(testValueCacheOptions);
+            builder.setMaxBatchSize(10);
+            builder.setStatisticsCollector(testStatisticsCollectorSupplier);
+        });
+
+        assertThat(builtOptions.batchingEnabled(),
+                equalTo(false));
+        assertThat(builtOptions.getBatchLoaderScheduler(),
+                equalTo(testBatchLoaderScheduler));
+        assertThat(builtOptions.getBatchLoaderContextProvider(),
+                equalTo(testBatchLoaderContextProvider));
+        assertThat(builtOptions.cacheMap().get(),
+                equalTo(testCacheMap));
+        assertThat(builtOptions.cachingEnabled(),
+                equalTo(false));
+        assertThat(builtOptions.getValueCacheOptions(),
+                equalTo(testValueCacheOptions));
+        assertThat(builtOptions.cacheKeyFunction().get(),
+                equalTo(testCacheKey));
+        assertThat(builtOptions.valueCache().get(),
+                equalTo(testValueCache));
+        assertThat(builtOptions.maxBatchSize(),
+                equalTo(10));
+        assertThat(builtOptions.getStatisticsCollector(),
+                equalTo(testStatisticsCollectorSupplier.get()));
+
+    }
+
+    @Test
+    void canBuildViaBuilderOk() {
+
+        DataLoaderOptions.Builder builder = DataLoaderOptions.newOptionsBuilder();
+        builder.setBatchingEnabled(false);
+        builder.setCachingExceptionsEnabled(false);
+        builder.setCachingEnabled(false);
+        builder.setBatchLoaderScheduler(testBatchLoaderScheduler);
+        builder.setBatchLoaderContextProvider(testBatchLoaderContextProvider);
+        builder.setCacheMap(testCacheMap);
+        builder.setValueCache(testValueCache);
+        builder.setCacheKeyFunction(testCacheKey);
+        builder.setValueCacheOptions(testValueCacheOptions);
+        builder.setMaxBatchSize(10);
+        builder.setStatisticsCollector(testStatisticsCollectorSupplier);
+
+        DataLoaderOptions builtOptions = builder.build();
+
+        assertThat(builtOptions.batchingEnabled(),
+                equalTo(false));
+        assertThat(builtOptions.getBatchLoaderScheduler(),
+                equalTo(testBatchLoaderScheduler));
+        assertThat(builtOptions.getBatchLoaderContextProvider(),
+                equalTo(testBatchLoaderContextProvider));
+        assertThat(builtOptions.cacheMap().get(),
+                equalTo(testCacheMap));
+        assertThat(builtOptions.cachingEnabled(),
+                equalTo(false));
+        assertThat(builtOptions.getValueCacheOptions(),
+                equalTo(testValueCacheOptions));
+        assertThat(builtOptions.cacheKeyFunction().get(),
+                equalTo(testCacheKey));
+        assertThat(builtOptions.valueCache().get(),
+                equalTo(testValueCache));
+        assertThat(builtOptions.maxBatchSize(),
+                equalTo(10));
+        assertThat(builtOptions.getStatisticsCollector(),
+                equalTo(testStatisticsCollectorSupplier.get()));
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/dataloader/ValueCacheOptionsTest.java b/src/test/java/org/dataloader/ValueCacheOptionsTest.java
new file mode 100644
index 0000000..469e291
--- /dev/null
+++ b/src/test/java/org/dataloader/ValueCacheOptionsTest.java
@@ -0,0 +1,19 @@
+package org.dataloader;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+class ValueCacheOptionsTest {
+
+    @Test
+    void saneDefaults() {
+        ValueCacheOptions newOptions = ValueCacheOptions.newOptions();
+        assertThat(newOptions.isCompleteValueAfterCacheSet(), equalTo(false));
+
+        ValueCacheOptions differentOptions = newOptions.setCompleteValueAfterCacheSet(true);
+        assertThat(differentOptions.isCompleteValueAfterCacheSet(), equalTo(true));
+        assertThat(differentOptions == newOptions, equalTo(false));
+    }
+}
\ No newline at end of file