Repository: jdbi/jdbi
Issue: test: Nondeterminstic `TestTimestampted` test failures

Observed in Travis build https://travis-ci.org/jdbi/jdbi/jobs/481605774

```
[ERROR] Failures: 
[ERROR]   TestTimestamped.shouldAllowCustomTimestampParameter:88 
Expecting:
 <2019-01-19T01:49:50.767+02:00>
to be between:
 [2019-01-18T23:49:50.766Z, 2019-01-18T23:49:50.767Z]
```

I'm not sure exactly what's going on with this failure.

Changed files:
sqlobject/src/test/java/org/jdbi/v3/sqlobject/MockClock.java
sqlobject/src/test/java/org/jdbi/v3/sqlobject/TestTimestamped.java
sqlobject/src/test/java/org/jdbi/v3/sqlobject/TestInheritedAnnotations.java
sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/internal/TimestampedFactory.java

diff --git a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/internal/TimestampedFactory.java b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/internal/TimestampedFactory.java
index b9db6a4b3..cec103c6a 100644
--- a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/internal/TimestampedFactory.java
+++ b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/internal/TimestampedFactory.java
@@ -15,22 +15,35 @@ package org.jdbi.v3.sqlobject.customizer.internal;
 
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Method;
+import java.time.Clock;
 import java.time.OffsetDateTime;
 import java.time.ZoneId;
+import java.util.function.Function;
 
+import org.jdbi.v3.meta.Beta;
 import org.jdbi.v3.sqlobject.customizer.SqlStatementCustomizer;
 import org.jdbi.v3.sqlobject.customizer.SqlStatementCustomizerFactory;
 import org.jdbi.v3.sqlobject.customizer.Timestamped;
 import org.jdbi.v3.sqlobject.customizer.TimestampedConfig;
 
 public class TimestampedFactory implements SqlStatementCustomizerFactory {
+    private static Function<ZoneId, Clock> timeSource = Clock::system;
+
     @Override
     public SqlStatementCustomizer createForMethod(Annotation annotation, Class<?> sqlObjectType, Method method) {
         final String parameterName = ((Timestamped) annotation).value();
 
         return stmt -> {
             ZoneId zone = stmt.getConfig(TimestampedConfig.class).getTimezone();
-            stmt.bind(parameterName, OffsetDateTime.now(zone));
+            stmt.bind(parameterName, OffsetDateTime.now(timeSource.apply(zone)));
         };
     }
+
+    /**
+     * for testing purposes only
+     */
+    @Beta
+    static void setTimeSource(Function<ZoneId, Clock> timeSource) {
+        TimestampedFactory.timeSource = timeSource;
+    }
 }
diff --git a/sqlobject/src/test/java/org/jdbi/v3/sqlobject/MockClock.java b/sqlobject/src/test/java/org/jdbi/v3/sqlobject/MockClock.java
index 1cf8f0a67..ec8f6ddc1 100644
--- a/sqlobject/src/test/java/org/jdbi/v3/sqlobject/MockClock.java
+++ b/sqlobject/src/test/java/org/jdbi/v3/sqlobject/MockClock.java
@@ -16,27 +16,41 @@ package org.jdbi.v3.sqlobject;
 import java.time.Clock;
 import java.time.Instant;
 import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.time.temporal.TemporalUnit;
 
-class MockClock extends Clock {
-    private Instant now = Instant.now();
+public class MockClock extends Clock {
+    private ZonedDateTime now;
+
+    private MockClock(ZonedDateTime now) {
+        this.now = now;
+    }
 
     @Override
     public ZoneId getZone() {
-        return ZoneId.systemDefault();
+        return now.getZone();
     }
 
     @Override
     public Clock withZone(ZoneId zone) {
-        throw new UnsupportedOperationException();
+        return new MockClock(now.withZoneSameInstant(zone));
     }
 
     @Override
     public Instant instant() {
-        return now;
+        return now.toInstant();
     }
 
     public Instant advance(long amountToAdd, TemporalUnit unit) {
-        return now = now.plus(amountToAdd, unit);
+        now = now.plus(amountToAdd, unit);
+        return instant();
+    }
+
+    public static MockClock now() {
+        return at(ZonedDateTime.now());
+    }
+
+    public static MockClock at(ZonedDateTime now) {
+        return new MockClock(now);
     }
 }
diff --git a/sqlobject/src/test/java/org/jdbi/v3/sqlobject/TestInheritedAnnotations.java b/sqlobject/src/test/java/org/jdbi/v3/sqlobject/TestInheritedAnnotations.java
index aac47aed4..a49eac0a1 100644
--- a/sqlobject/src/test/java/org/jdbi/v3/sqlobject/TestInheritedAnnotations.java
+++ b/sqlobject/src/test/java/org/jdbi/v3/sqlobject/TestInheritedAnnotations.java
@@ -37,7 +37,7 @@ public class TestInheritedAnnotations {
     @Rule
     public H2DatabaseRule dbRule = new H2DatabaseRule().withPlugin(new SqlObjectPlugin());
 
-    private MockClock mockClock = new MockClock();
+    private MockClock mockClock = MockClock.now();
 
     @Before
     public void setUp() {
diff --git a/sqlobject/src/test/java/org/jdbi/v3/sqlobject/TestTimestamped.java b/sqlobject/src/test/java/org/jdbi/v3/sqlobject/customizer/internal/TestTimestamped.java
similarity index 84%
rename from sqlobject/src/test/java/org/jdbi/v3/sqlobject/TestTimestamped.java
rename to sqlobject/src/test/java/org/jdbi/v3/sqlobject/customizer/internal/TestTimestamped.java
index b6ed8edae..375a9bde5 100644
--- a/sqlobject/src/test/java/org/jdbi/v3/sqlobject/TestTimestamped.java
+++ b/sqlobject/src/test/java/org/jdbi/v3/sqlobject/customizer/internal/TestTimestamped.java
@@ -11,18 +11,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.jdbi.v3.sqlobject;
+package org.jdbi.v3.sqlobject.customizer.internal;
 
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.Month;
 import java.time.OffsetDateTime;
 import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
 
 import org.jdbi.v3.core.Jdbi;
 import org.jdbi.v3.core.mapper.RowMapper;
 import org.jdbi.v3.core.statement.SqlLogger;
 import org.jdbi.v3.core.statement.StatementContext;
+import org.jdbi.v3.sqlobject.MockClock;
+import org.jdbi.v3.sqlobject.SqlObjectPlugin;
 import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
 import org.jdbi.v3.sqlobject.customizer.Bind;
 import org.jdbi.v3.sqlobject.customizer.BindBean;
@@ -42,22 +48,25 @@ import static org.assertj.core.api.Assertions.assertThat;
  * Tests for the {@link Timestamped} annotation
  */
 public class TestTimestamped {
-    private static final ZoneOffset OFFSET = ZoneOffset.ofHours(2);
+    private static final ZoneOffset GMT_PLUS_2 = ZoneOffset.ofHours(2);
+    private static final OffsetDateTime UTC_MOMENT = OffsetDateTime.of(LocalDate.of(2018, Month.JANUARY, 1), LocalTime.NOON, ZoneOffset.UTC);
 
     @Rule
     public JdbiRule dbRule = JdbiRule.h2().withPlugin(new SqlObjectPlugin());
-
     private PersonDAO personDAO;
-    private OffsetDateTime timestamp;
-    private Timestamp sqlTimestamp;
-    private OffsetDateTime testStart;
+
+    private OffsetDateTime insertedTimestamp;
+    private Timestamp insertedSqlTimestamp;
+
+    private final MockClock clock = MockClock.at(UTC_MOMENT.toZonedDateTime());
 
     @Before
     public void before() {
+        TimestampedFactory.setTimeSource(clock::withZone);
+        dbRule.getJdbi().getConfig(TimestampedConfig.class).setTimezone(GMT_PLUS_2);
+
         personDAO = dbRule.getJdbi().onDemand(PersonDAO.class);
         personDAO.createTable();
-        dbRule.getJdbi().getConfig(TimestampedConfig.class).setTimezone(OFFSET);
-        testStart = OffsetDateTime.now();
     }
 
     @Test
@@ -67,14 +76,14 @@ public class TestTimestamped {
 
         recordNextTimestamp("now");
         personDAO.insert(input);
-        assertThat(timestamp.getOffset()).isEqualTo(OFFSET);
-        assertThat(timestamp).isBetween(testStart, OffsetDateTime.now());
+        assertThat(insertedTimestamp.getOffset()).isEqualTo(GMT_PLUS_2);
+        assertThat(insertedTimestamp.toInstant()).isEqualTo(UTC_MOMENT.toInstant());
 
         Person result = personDAO.get(1);
 
         assertThat(result.getCreated())
             .isEqualTo(result.getModified())
-            .isEqualTo(sqlTimestamp);
+            .isEqualTo(insertedSqlTimestamp);
     }
 
     @Test
@@ -84,8 +93,8 @@ public class TestTimestamped {
 
         recordNextTimestamp("createdAt");
         personDAO.insertWithCustomTimestampFields(input);
-        assertThat(timestamp.getOffset()).isEqualTo(OFFSET);
-        assertThat(timestamp).isBetween(testStart, OffsetDateTime.now());
+        assertThat(insertedTimestamp.getOffset()).isEqualTo(GMT_PLUS_2);
+        assertThat(insertedTimestamp.toInstant()).isEqualTo(UTC_MOMENT.toInstant());
 
         Person result = personDAO.get(1);
 
@@ -93,7 +102,7 @@ public class TestTimestamped {
         assertThat(result.getLastName()).isEqualTo(input.getLastName());
         assertThat(result.getCreated())
             .isEqualTo(result.getModified())
-            .isEqualTo(sqlTimestamp);
+            .isEqualTo(insertedSqlTimestamp);
     }
 
     @Test
@@ -103,17 +112,19 @@ public class TestTimestamped {
 
         recordNextTimestamp("now");
         personDAO.insert(input);
-        Timestamp insert = sqlTimestamp;
+        Timestamp insert = insertedSqlTimestamp;
 
         Person fetched = personDAO.get(3);
         fetched.setLastName("Banda");
+        clock.advance(1, ChronoUnit.SECONDS);
 
         recordNextTimestamp("now");
         personDAO.updatePerson(fetched);
-        Timestamp update = sqlTimestamp;
+        Timestamp update = insertedSqlTimestamp;
 
         Person result = personDAO.get(3);
 
+        assertThat(insert).isNotEqualTo(update);
         assertThat(result.getLastName()).isEqualToIgnoringCase("Banda");
         assertThat(result.getCreated()).isEqualTo(insert);
         assertThat(result.getModified()).isEqualTo(update);
@@ -151,8 +162,8 @@ public class TestTimestamped {
                     .findForName(name, ctx)
                     .orElseThrow(AssertionError::new)
                     .toString();
-                timestamp = OffsetDateTime.parse(toString);
-                sqlTimestamp = Timestamp.from(timestamp.toInstant());
+                insertedTimestamp = OffsetDateTime.parse(toString);
+                insertedSqlTimestamp = Timestamp.from(insertedTimestamp.toInstant());
             }
 
             @Override