From 5c7fc8494290e5b391e944de7ed2d1e48e703180 Mon Sep 17 00:00:00 2001 From: Jordan Sim-Smith <jordansimsmith@canva.com> Date: Mon, 26 Aug 2024 17:43:11 +1200 Subject: [PATCH] Refactor dependency injection to support test implementations --- immersion_tracker_api/BUILD.bazel | 15 ++++++--- .../immersiontracker/GetProgressHandler.java | 7 ++-- .../ImmersionTrackerFactory.java | 32 ++++++++---------- .../ImmersionTrackerModule.java | 27 --------------- .../GetProgressHandlerIntegrationTest.java | 31 ++++++++++------- .../ImmersionTrackerTestFactory.java | 33 +++++++++++++++++++ lib/dynamodb/BUILD.bazel | 16 +++++++++ .../dynamodb/DynamoDbModule.java | 22 +++++++++++++ .../dynamodb/DynamoDbTestModule.java | 24 ++++++++++++++ lib/json/BUILD.bazel | 15 +++++++++ .../json/ObjectMapperModule.java | 13 ++++++++ lib/time/BUILD.bazel | 14 ++++++++ .../java/com/jordansimsmith/time/Clock.java | 7 ++++ .../com/jordansimsmith/time/ClockModule.java | 12 +++++++ .../jordansimsmith/time/ClockTestModule.java | 17 ++++++++++ .../com/jordansimsmith/time/FakeClock.java | 22 +++++++++++++ .../com/jordansimsmith/time/SystemClock.java | 10 ++++++ 17 files changed, 254 insertions(+), 63 deletions(-) create mode 100644 immersion_tracker_api/src/test/java/com/jordansimsmith/immersiontracker/ImmersionTrackerTestFactory.java create mode 100644 lib/dynamodb/BUILD.bazel create mode 100644 lib/dynamodb/src/main/java/com/jordansimsmith/dynamodb/DynamoDbModule.java create mode 100644 lib/dynamodb/src/main/java/com/jordansimsmith/dynamodb/DynamoDbTestModule.java create mode 100644 lib/json/BUILD.bazel create mode 100644 lib/json/src/main/java/com/jordansimsmith/json/ObjectMapperModule.java create mode 100644 lib/time/BUILD.bazel create mode 100644 lib/time/src/main/java/com/jordansimsmith/time/Clock.java create mode 100644 lib/time/src/main/java/com/jordansimsmith/time/ClockModule.java create mode 100644 lib/time/src/main/java/com/jordansimsmith/time/ClockTestModule.java create mode 100644 lib/time/src/main/java/com/jordansimsmith/time/FakeClock.java create mode 100644 lib/time/src/main/java/com/jordansimsmith/time/SystemClock.java diff --git a/immersion_tracker_api/BUILD.bazel b/immersion_tracker_api/BUILD.bazel index 77647cf..e2cc55e 100644 --- a/immersion_tracker_api/BUILD.bazel +++ b/immersion_tracker_api/BUILD.bazel @@ -13,11 +13,12 @@ java_library( ), deps = [ ":dagger", - "@maven//:com_google_code_findbugs_jsr305", - "@maven//:com_google_guava_guava", + "//lib/dynamodb:lib", + "//lib/json:lib", + "//lib/time:lib", + "@maven//:com_fasterxml_jackson_core_jackson_databind", "@maven//:software_amazon_awssdk_dynamodb", "@maven//:software_amazon_awssdk_dynamodb_enhanced", - "@maven//:com_fasterxml_jackson_core_jackson_databind", ], ) @@ -30,6 +31,7 @@ java_binary( ]), deps = [ ":lib", + "//lib/time:lib", "@maven//:ch_qos_logback_logback_classic", "@maven//:ch_qos_logback_logback_core", "@maven//:com_amazonaws_aws_lambda_java_core", @@ -38,7 +40,6 @@ java_binary( "@maven//:com_fasterxml_jackson_core_jackson_databind", "@maven//:com_google_code_findbugs_jsr305", "@maven//:com_google_guava_guava", - "@maven//:software_amazon_awssdk_dynamodb", "@maven//:software_amazon_awssdk_dynamodb_enhanced", ], ) @@ -57,12 +58,16 @@ java_test_suite( test_suffixes = ["IntegrationTest.java"], runtime_deps = JUNIT5_DEPS, deps = [ + ":dagger", ":get-progress-handler", ":lib", + "//lib/dynamodb:lib", + "//lib/json:lib", "//lib/testcontainers:lib", + "//lib/time:lib", "@maven//:com_amazonaws_aws_lambda_java_core", - "@maven//:com_fasterxml_jackson_core_jackson_databind", "@maven//:com_amazonaws_aws_lambda_java_events", + "@maven//:com_fasterxml_jackson_core_jackson_databind", "@maven//:org_assertj_assertj_core", "@maven//:org_junit_jupiter_junit_jupiter_api", "@maven//:org_testcontainers_junit_jupiter", diff --git a/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/GetProgressHandler.java b/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/GetProgressHandler.java index 2868d52..073204c 100644 --- a/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/GetProgressHandler.java +++ b/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/GetProgressHandler.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; +import com.jordansimsmith.time.Clock; import java.time.Instant; import java.time.ZoneId; import java.time.temporal.ChronoUnit; @@ -23,8 +24,9 @@ public class GetProgressHandler implements RequestHandler<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> { private static final int MINUTES_PER_EPISODE = 20; - private static final ZoneId AUCKLAND_ZONE_ID = ZoneId.of("Pacific/Auckland"); + @VisibleForTesting static final ZoneId ZONE_ID = ZoneId.of("Pacific/Auckland"); + private final Clock clock; private final ObjectMapper objectMapper; private final DynamoDbTable<ImmersionTrackerItem> immersionTrackerTable; @@ -48,6 +50,7 @@ public GetProgressHandler() { @VisibleForTesting GetProgressHandler(ImmersionTrackerFactory factory) { + this.clock = factory.clock(); this.objectMapper = factory.objectMapper(); this.immersionTrackerTable = factory.immersionTrackerTable(); } @@ -85,7 +88,7 @@ private APIGatewayV2HTTPResponse doHandleRequest(APIGatewayV2HTTPEvent event, Co var totalEpisodesWatched = episodes.size(); var totalHoursWatched = totalEpisodesWatched * MINUTES_PER_EPISODE / 60; - var today = Instant.now().atZone(AUCKLAND_ZONE_ID).truncatedTo(ChronoUnit.DAYS).toInstant(); + var today = clock.now().atZone(ZONE_ID).truncatedTo(ChronoUnit.DAYS).toInstant(); var episodesWatchedToday = episodes.stream() .filter(e -> Instant.ofEpochSecond(e.getTimestamp()).isAfter(today)) diff --git a/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/ImmersionTrackerFactory.java b/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/ImmersionTrackerFactory.java index a820dc9..e6111c2 100644 --- a/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/ImmersionTrackerFactory.java +++ b/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/ImmersionTrackerFactory.java @@ -1,19 +1,27 @@ package com.jordansimsmith.immersiontracker; import com.fasterxml.jackson.databind.ObjectMapper; -import dagger.BindsInstance; +import com.jordansimsmith.dynamodb.DynamoDbModule; +import com.jordansimsmith.json.ObjectMapperModule; +import com.jordansimsmith.time.Clock; +import com.jordansimsmith.time.ClockModule; import dagger.Component; -import java.net.URI; -import javax.annotation.Nullable; -import javax.inject.Named; import javax.inject.Singleton; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; @Singleton -@Component(modules = {ImmersionTrackerModule.class}) +@Component( + modules = { + ClockModule.class, + ObjectMapperModule.class, + DynamoDbModule.class, + ImmersionTrackerModule.class + }) public interface ImmersionTrackerFactory { + Clock clock(); + ObjectMapper objectMapper(); DynamoDbClient dynamoDbClient(); @@ -22,19 +30,7 @@ public interface ImmersionTrackerFactory { DynamoDbTable<ImmersionTrackerItem> immersionTrackerTable(); - @Component.Builder - interface Builder { - @BindsInstance - Builder dynamoDbEndpoint(@Named("dynamoDbEndpoint") @Nullable URI dynamoDbEndpoint); - - ImmersionTrackerFactory build(); - } - static ImmersionTrackerFactory create() { - return DaggerImmersionTrackerFactory.builder().build(); - } - - static ImmersionTrackerFactory.Builder builder() { - return DaggerImmersionTrackerFactory.builder(); + return DaggerImmersionTrackerFactory.create(); } } diff --git a/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/ImmersionTrackerModule.java b/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/ImmersionTrackerModule.java index 631c34a..179df47 100644 --- a/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/ImmersionTrackerModule.java +++ b/immersion_tracker_api/src/main/java/com/jordansimsmith/immersiontracker/ImmersionTrackerModule.java @@ -1,41 +1,14 @@ package com.jordansimsmith.immersiontracker; -import com.fasterxml.jackson.databind.ObjectMapper; import dagger.Module; import dagger.Provides; -import java.net.URI; -import javax.annotation.Nullable; -import javax.inject.Named; import javax.inject.Singleton; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; @Module public class ImmersionTrackerModule { - @Provides - public ObjectMapper objectMapper() { - return new ObjectMapper(); - } - - @Provides - @Singleton - public DynamoDbClient dynamoDbClient(@Named("dynamoDbEndpoint") @Nullable URI dynamoDbEndpoint) { - var builder = DynamoDbClient.builder(); - if (dynamoDbEndpoint != null) { - builder.endpointOverride(dynamoDbEndpoint); - } - - return builder.build(); - } - - @Provides - @Singleton - public DynamoDbEnhancedClient dynamoDbEnhancedClient(DynamoDbClient dynamoDbClient) { - return DynamoDbEnhancedClient.builder().dynamoDbClient(dynamoDbClient).build(); - } - @Provides @Singleton public DynamoDbTable<ImmersionTrackerItem> immersionTrackerTable( diff --git a/immersion_tracker_api/src/test/java/com/jordansimsmith/immersiontracker/GetProgressHandlerIntegrationTest.java b/immersion_tracker_api/src/test/java/com/jordansimsmith/immersiontracker/GetProgressHandlerIntegrationTest.java index 5559a10..105c0b7 100644 --- a/immersion_tracker_api/src/test/java/com/jordansimsmith/immersiontracker/GetProgressHandlerIntegrationTest.java +++ b/immersion_tracker_api/src/test/java/com/jordansimsmith/immersiontracker/GetProgressHandlerIntegrationTest.java @@ -1,8 +1,11 @@ package com.jordansimsmith.immersiontracker; +import static org.assertj.core.api.Assertions.assertThat; + import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; import com.fasterxml.jackson.databind.ObjectMapper; import com.jordansimsmith.testcontainers.DynamoDbContainer; +import com.jordansimsmith.time.FakeClock; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.junit.jupiter.Container; @@ -10,29 +13,31 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.services.dynamodb.waiters.DynamoDbWaiter; -import static org.assertj.core.api.Assertions.assertThat; - @Testcontainers public class GetProgressHandlerIntegrationTest { + private FakeClock fakeClock; private ObjectMapper objectMapper; private DynamoDbTable<ImmersionTrackerItem> immersionTrackerTable; private GetProgressHandler getProgressHandler; - @Container - DynamoDbContainer dynamoDbContainer = new DynamoDbContainer(); + @Container DynamoDbContainer dynamoDbContainer = new DynamoDbContainer(); @BeforeEach void setUp() { - var factory = ImmersionTrackerFactory.builder().dynamoDbEndpoint(dynamoDbContainer.getEndpoint()).build(); + var factory = ImmersionTrackerTestFactory.create(dynamoDbContainer.getEndpoint()); + fakeClock = factory.fakeClock(); objectMapper = factory.objectMapper(); var dynamoDbClient = factory.dynamoDbClient(); immersionTrackerTable = factory.immersionTrackerTable(); immersionTrackerTable.createTable(); try (var waiter = DynamoDbWaiter.builder().client(dynamoDbClient).build()) { - var res = waiter.waitUntilTableExists(b -> b.tableName(immersionTrackerTable.tableName()).build()).matched(); + var res = + waiter + .waitUntilTableExists(b -> b.tableName(immersionTrackerTable.tableName()).build()) + .matched(); res.response().orElseThrow(); } @@ -40,11 +45,15 @@ void setUp() { } @Test - void handleRequestShouldQueryItems() throws Exception { + void handleRequestShouldCalculateProgress() throws Exception { // arrange - var episode1 = ImmersionTrackerItem.createEpisode("jordansimsmith", "show1", "episode1", 123); - var episode2 = ImmersionTrackerItem.createEpisode("jordansimsmith", "show1", "episode2", 456); - var episode3 = ImmersionTrackerItem.createEpisode("jordansimsmith", "show2", "episode1", 789); + var now = (int) fakeClock.now().atZone(GetProgressHandler.ZONE_ID).toInstant().getEpochSecond(); + var episode1 = + ImmersionTrackerItem.createEpisode("jordansimsmith", "show1", "episode1", now - 100); + var episode2 = + ImmersionTrackerItem.createEpisode("jordansimsmith", "show1", "episode2", now + 100); + var episode3 = + ImmersionTrackerItem.createEpisode("jordansimsmith", "show2", "episode1", now - 100); var show = ImmersionTrackerItem.createShow("jordansimsmith", "show1"); show.setTvdbId(1); show.setTvdbName("my show"); @@ -65,7 +74,7 @@ void handleRequestShouldQueryItems() throws Exception { assertThat(progress).isNotNull(); assertThat(progress.totalEpisodesWatched()).isEqualTo(3); assertThat(progress.totalHoursWatched()).isEqualTo(1); - assertThat(progress.episodesWatchedToday()).isEqualTo(0); + assertThat(progress.episodesWatchedToday()).isEqualTo(1); var shows = progress.shows(); assertThat(shows).hasSize(2); diff --git a/immersion_tracker_api/src/test/java/com/jordansimsmith/immersiontracker/ImmersionTrackerTestFactory.java b/immersion_tracker_api/src/test/java/com/jordansimsmith/immersiontracker/ImmersionTrackerTestFactory.java new file mode 100644 index 0000000..a3ce79d --- /dev/null +++ b/immersion_tracker_api/src/test/java/com/jordansimsmith/immersiontracker/ImmersionTrackerTestFactory.java @@ -0,0 +1,33 @@ +package com.jordansimsmith.immersiontracker; + +import com.jordansimsmith.dynamodb.DynamoDbTestModule; +import com.jordansimsmith.json.ObjectMapperModule; +import com.jordansimsmith.time.ClockTestModule; +import com.jordansimsmith.time.FakeClock; +import dagger.BindsInstance; +import dagger.Component; +import java.net.URI; +import javax.inject.Named; +import javax.inject.Singleton; + +@Singleton +@Component( + modules = { + ClockTestModule.class, + ObjectMapperModule.class, + DynamoDbTestModule.class, + ImmersionTrackerModule.class + }) +public interface ImmersionTrackerTestFactory extends ImmersionTrackerFactory { + FakeClock fakeClock(); + + @Component.Factory + interface Factory { + ImmersionTrackerTestFactory create( + @BindsInstance @Named("dynamoDbEndpoint") URI dynamoDbEndpoint); + } + + static ImmersionTrackerTestFactory create(URI dynamoDbEndpoint) { + return DaggerImmersionTrackerTestFactory.factory().create(dynamoDbEndpoint); + } +} diff --git a/lib/dynamodb/BUILD.bazel b/lib/dynamodb/BUILD.bazel new file mode 100644 index 0000000..c23be34 --- /dev/null +++ b/lib/dynamodb/BUILD.bazel @@ -0,0 +1,16 @@ +load("@dagger//:workspace_defs.bzl", "dagger_rules") + +dagger_rules() + +java_library( + name = "lib", + srcs = glob(["src/main/java/**/*.java"]), + visibility = [ + "//visibility:public", + ], + deps = [ + ":dagger", + "@maven//:software_amazon_awssdk_dynamodb", + "@maven//:software_amazon_awssdk_dynamodb_enhanced", + ], +) diff --git a/lib/dynamodb/src/main/java/com/jordansimsmith/dynamodb/DynamoDbModule.java b/lib/dynamodb/src/main/java/com/jordansimsmith/dynamodb/DynamoDbModule.java new file mode 100644 index 0000000..622e06a --- /dev/null +++ b/lib/dynamodb/src/main/java/com/jordansimsmith/dynamodb/DynamoDbModule.java @@ -0,0 +1,22 @@ +package com.jordansimsmith.dynamodb; + +import dagger.Module; +import dagger.Provides; +import javax.inject.Singleton; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +@Module +public class DynamoDbModule { + @Provides + @Singleton + public DynamoDbClient dynamoDbClient() { + return DynamoDbClient.builder().build(); + } + + @Provides + @Singleton + public DynamoDbEnhancedClient dynamoDbEnhancedClient(DynamoDbClient dynamoDbClient) { + return DynamoDbEnhancedClient.builder().dynamoDbClient(dynamoDbClient).build(); + } +} diff --git a/lib/dynamodb/src/main/java/com/jordansimsmith/dynamodb/DynamoDbTestModule.java b/lib/dynamodb/src/main/java/com/jordansimsmith/dynamodb/DynamoDbTestModule.java new file mode 100644 index 0000000..642227b --- /dev/null +++ b/lib/dynamodb/src/main/java/com/jordansimsmith/dynamodb/DynamoDbTestModule.java @@ -0,0 +1,24 @@ +package com.jordansimsmith.dynamodb; + +import dagger.Module; +import dagger.Provides; +import java.net.URI; +import javax.inject.Named; +import javax.inject.Singleton; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +@Module +public class DynamoDbTestModule { + @Provides + @Singleton + public DynamoDbClient dynamoDbClient(@Named("dynamoDbEndpoint") URI dynamoDbEndpoint) { + return DynamoDbClient.builder().endpointOverride(dynamoDbEndpoint).build(); + } + + @Provides + @Singleton + public DynamoDbEnhancedClient dynamoDbEnhancedClient(DynamoDbClient dynamoDbClient) { + return DynamoDbEnhancedClient.builder().dynamoDbClient(dynamoDbClient).build(); + } +} diff --git a/lib/json/BUILD.bazel b/lib/json/BUILD.bazel new file mode 100644 index 0000000..c4f3b44 --- /dev/null +++ b/lib/json/BUILD.bazel @@ -0,0 +1,15 @@ +load("@dagger//:workspace_defs.bzl", "dagger_rules") + +dagger_rules() + +java_library( + name = "lib", + srcs = glob(["src/main/java/**/*.java"]), + visibility = [ + "//visibility:public", + ], + deps = [ + ":dagger", + "@maven//:com_fasterxml_jackson_core_jackson_databind", + ], +) diff --git a/lib/json/src/main/java/com/jordansimsmith/json/ObjectMapperModule.java b/lib/json/src/main/java/com/jordansimsmith/json/ObjectMapperModule.java new file mode 100644 index 0000000..5d6f2c0 --- /dev/null +++ b/lib/json/src/main/java/com/jordansimsmith/json/ObjectMapperModule.java @@ -0,0 +1,13 @@ +package com.jordansimsmith.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dagger.Module; +import dagger.Provides; + +@Module +public class ObjectMapperModule { + @Provides + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } +} diff --git a/lib/time/BUILD.bazel b/lib/time/BUILD.bazel new file mode 100644 index 0000000..7ecf491 --- /dev/null +++ b/lib/time/BUILD.bazel @@ -0,0 +1,14 @@ +load("@dagger//:workspace_defs.bzl", "dagger_rules") + +dagger_rules() + +java_library( + name = "lib", + srcs = glob(["src/main/java/**/*.java"]), + visibility = [ + "//visibility:public", + ], + deps = [ + ":dagger", + ], +) diff --git a/lib/time/src/main/java/com/jordansimsmith/time/Clock.java b/lib/time/src/main/java/com/jordansimsmith/time/Clock.java new file mode 100644 index 0000000..250f933 --- /dev/null +++ b/lib/time/src/main/java/com/jordansimsmith/time/Clock.java @@ -0,0 +1,7 @@ +package com.jordansimsmith.time; + +import java.time.Instant; + +public interface Clock { + Instant now(); +} diff --git a/lib/time/src/main/java/com/jordansimsmith/time/ClockModule.java b/lib/time/src/main/java/com/jordansimsmith/time/ClockModule.java new file mode 100644 index 0000000..5028cbf --- /dev/null +++ b/lib/time/src/main/java/com/jordansimsmith/time/ClockModule.java @@ -0,0 +1,12 @@ +package com.jordansimsmith.time; + +import dagger.Module; +import dagger.Provides; + +@Module +public class ClockModule { + @Provides + public Clock clock() { + return new SystemClock(); + } +} diff --git a/lib/time/src/main/java/com/jordansimsmith/time/ClockTestModule.java b/lib/time/src/main/java/com/jordansimsmith/time/ClockTestModule.java new file mode 100644 index 0000000..f7981ca --- /dev/null +++ b/lib/time/src/main/java/com/jordansimsmith/time/ClockTestModule.java @@ -0,0 +1,17 @@ +package com.jordansimsmith.time; + +import dagger.Module; +import dagger.Provides; + +@Module +public class ClockTestModule { + @Provides + public FakeClock fakeClock() { + return new FakeClock(); + } + + @Provides + public Clock clock(FakeClock fakeClock) { + return fakeClock; + } +} diff --git a/lib/time/src/main/java/com/jordansimsmith/time/FakeClock.java b/lib/time/src/main/java/com/jordansimsmith/time/FakeClock.java new file mode 100644 index 0000000..70c3183 --- /dev/null +++ b/lib/time/src/main/java/com/jordansimsmith/time/FakeClock.java @@ -0,0 +1,22 @@ +package com.jordansimsmith.time; + +import java.time.Instant; + +public class FakeClock implements Clock { + private static final long DEFAULT_EPOCH_MILLI = 946638000000L; // 2000-01-01 + + private long currentEpochMilli = DEFAULT_EPOCH_MILLI; + + @Override + public Instant now() { + return Instant.ofEpochMilli(currentEpochMilli); + } + + public void setTime(long epochMillis) { + this.currentEpochMilli = epochMillis; + } + + public void reset() { + currentEpochMilli = DEFAULT_EPOCH_MILLI; + } +} diff --git a/lib/time/src/main/java/com/jordansimsmith/time/SystemClock.java b/lib/time/src/main/java/com/jordansimsmith/time/SystemClock.java new file mode 100644 index 0000000..03f63c4 --- /dev/null +++ b/lib/time/src/main/java/com/jordansimsmith/time/SystemClock.java @@ -0,0 +1,10 @@ +package com.jordansimsmith.time; + +import java.time.Instant; + +public class SystemClock implements Clock { + @Override + public Instant now() { + return Instant.now(); + } +}