diff --git a/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcResultSet.java b/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcResultSet.java
index a85a3d3de244..6016bd2c3033 100644
--- a/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcResultSet.java
+++ b/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcResultSet.java
@@ -38,6 +38,8 @@
 abstract class AbstractJdbcResultSet extends AbstractJdbcWrapper implements ResultSet {
   /** The underlying Cloud Spanner {@link com.google.cloud.spanner.ResultSet}. */
   final com.google.cloud.spanner.ResultSet spanner;
+  /** Current fetch size hint for this result set. */
+  private int fetchSize;
 
   AbstractJdbcResultSet(com.google.cloud.spanner.ResultSet spanner) {
     this.spanner = spanner;
@@ -108,12 +110,12 @@ public int getFetchDirection() throws SQLException {
 
   @Override
   public void setFetchSize(int rows) throws SQLException {
-    // no-op
+    this.fetchSize = rows;
   }
 
   @Override
   public int getFetchSize() throws SQLException {
-    return 0;
+    return fetchSize;
   }
 
   @Override
diff --git a/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/AbstractJdbcResultSetTest.java b/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/AbstractJdbcResultSetTest.java
new file mode 100644
index 000000000000..a267d224fc15
--- /dev/null
+++ b/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/AbstractJdbcResultSetTest.java
@@ -0,0 +1,855 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.spanner.jdbc;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+
+import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl;
+import com.google.common.collect.Sets;
+import com.google.rpc.Code;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.Clob;
+import java.sql.Date;
+import java.sql.NClob;
+import java.sql.Ref;
+import java.sql.ResultSet;
+import java.sql.RowId;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.SQLXML;
+import java.sql.Statement;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class AbstractJdbcResultSetTest {
+
+  private JdbcResultSet rs;
+
+  @Before
+  public void setup() {
+    rs = JdbcResultSet.of(mock(Statement.class), JdbcResultSetTest.getMockResultSet());
+  }
+
+  @Test
+  public void testFetchSize() throws SQLException {
+    assertThat(rs.getFetchSize(), is(equalTo(0)));
+    for (int size : new int[] {0, 1, 10, 100}) {
+      rs.setFetchSize(size);
+      assertThat(rs.getFetchSize(), is(equalTo(size)));
+    }
+  }
+
+  @Test
+  public void testGetType() throws SQLException {
+    assertThat(rs.getType(), is(equalTo(ResultSet.TYPE_FORWARD_ONLY)));
+  }
+
+  @Test
+  public void testGetConcurrency() throws SQLException {
+    assertThat(rs.getConcurrency(), is(equalTo(ResultSet.CONCUR_READ_ONLY)));
+  }
+
+  @Test
+  public void testRowInsertedUpdatedDeleted() throws SQLException {
+    assertThat(rs.rowInserted(), is(false));
+    assertThat(rs.rowUpdated(), is(false));
+    assertThat(rs.rowDeleted(), is(false));
+  }
+
+  @Test
+  public void testGetFetchDirection() throws SQLException {
+    assertThat(rs.getFetchDirection(), is(equalTo(ResultSet.FETCH_FORWARD)));
+  }
+
+  @Test
+  public void testSetFetchDirection() throws SQLException {
+    Set<Integer> supported = Sets.newHashSet(ResultSet.FETCH_FORWARD);
+    for (int direction :
+        new int[] {ResultSet.FETCH_FORWARD, ResultSet.FETCH_REVERSE, ResultSet.FETCH_UNKNOWN}) {
+      try {
+        rs.setFetchDirection(direction);
+        assertThat(supported.contains(direction), is(true));
+      } catch (JdbcSqlExceptionImpl e) {
+        assertThat(supported.contains(direction), is(false));
+        assertThat(e.getCode(), is(equalTo(Code.INVALID_ARGUMENT)));
+      }
+    }
+  }
+
+  private static interface SqlRunnable {
+    void run() throws SQLException;
+  }
+
+  @Test
+  public void testUnsupportedFeatures() throws SQLException {
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.getCursorName();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.isLast();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.beforeFirst();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.afterLast();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.first();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.last();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.absolute(1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.relative(1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.previous();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNull(1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNull("test");
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBoolean(1, Boolean.TRUE);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBoolean("test", Boolean.TRUE);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateByte(1, (byte) 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateByte("test", (byte) 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateShort(1, (short) 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateShort("test", (short) 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateInt(1, 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateInt("test", 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateLong(1, 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateLong("test", 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateFloat(1, 1F);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateFloat("test", 1F);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateDouble(1, 1D);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateDouble("test", 1D);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBigDecimal(1, BigDecimal.ONE);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBigDecimal("test", BigDecimal.ONE);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateString(1, "value");
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateString("test", "value");
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBytes(1, "value".getBytes());
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBytes("test", "value".getBytes());
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateDate(1, new Date(System.currentTimeMillis()));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateDate("test", new Date(System.currentTimeMillis()));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateTimestamp(1, new Timestamp(System.currentTimeMillis()));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateTimestamp("test", new Timestamp(System.currentTimeMillis()));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateTime(1, new Time(System.currentTimeMillis()));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateTime("test", new Time(System.currentTimeMillis()));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateAsciiStream(1, new ByteArrayInputStream("value".getBytes()));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateAsciiStream("test", new ByteArrayInputStream("value".getBytes()));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateAsciiStream(1, new ByteArrayInputStream("value".getBytes()), 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateAsciiStream("test", new ByteArrayInputStream("value".getBytes()), 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateAsciiStream(1, new ByteArrayInputStream("value".getBytes()), 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateAsciiStream("test", new ByteArrayInputStream("value".getBytes()), 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBinaryStream(1, new ByteArrayInputStream("value".getBytes()));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBinaryStream("test", new ByteArrayInputStream("value".getBytes()));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBinaryStream(1, new ByteArrayInputStream("value".getBytes()), 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBinaryStream("test", new ByteArrayInputStream("value".getBytes()), 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBinaryStream(1, new ByteArrayInputStream("value".getBytes()), 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBinaryStream("test", new ByteArrayInputStream("value".getBytes()), 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateCharacterStream(1, new StringReader("value"));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateCharacterStream("test", new StringReader("value"));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateCharacterStream(1, new StringReader("value"), 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateCharacterStream("test", new StringReader("value"), 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateCharacterStream(1, new StringReader("value"), 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateCharacterStream("test", new StringReader("value"), 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateObject(1, new Object());
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateObject("test", new Object());
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateObject(1, new Object(), 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateObject("test", new Object(), 1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.insertRow();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateRow();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.deleteRow();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.refreshRow();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.cancelRowUpdates();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.moveToInsertRow();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.moveToCurrentRow();
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.getRef(1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.getRef("test");
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateRef(1, mock(Ref.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateRef("test", mock(Ref.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBlob(1, mock(Blob.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBlob("test", mock(Blob.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBlob(1, mock(InputStream.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBlob("test", mock(InputStream.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBlob(1, mock(InputStream.class), 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateBlob("test", mock(InputStream.class), 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateClob(1, mock(Clob.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateClob("test", mock(Clob.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateClob(1, mock(Reader.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateClob("test", mock(Reader.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateClob(1, mock(Reader.class), 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateClob("test", mock(Reader.class), 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateArray(1, mock(Array.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateArray("test", mock(Array.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.getRowId(1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.getRowId("test");
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateRowId(1, mock(RowId.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateRowId("test", mock(RowId.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNString(1, "value");
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNString("test", "value");
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNClob(1, mock(NClob.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNClob("test", mock(NClob.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNClob(1, mock(Reader.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNClob("test", mock(Reader.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNClob(1, mock(Reader.class), 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNClob("test", mock(Reader.class), 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.getSQLXML(1);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.getSQLXML("test");
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateSQLXML(1, mock(SQLXML.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateSQLXML("test", mock(SQLXML.class));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNCharacterStream(1, new StringReader("value"));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNCharacterStream("test", new StringReader("value"));
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNCharacterStream(1, new StringReader("value"), 1L);
+          }
+        });
+    assertUnsupported(
+        new SqlRunnable() {
+          @Override
+          public void run() throws SQLException {
+            rs.updateNCharacterStream("test", new StringReader("value"), 1L);
+          }
+        });
+  }
+
+  private <T> void assertUnsupported(SqlRunnable runnable) throws SQLException {
+    try {
+      runnable.run();
+      fail("missing expected SQLFeatureNotSupportedException");
+    } catch (SQLFeatureNotSupportedException e) {
+      // ignore, this is the expected exception.
+    } catch (Exception e) {
+      throw JdbcSqlExceptionFactory.of("unexpected exception", Code.INTERNAL, e);
+    }
+  }
+}
diff --git a/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java b/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java
index dbe75a86437c..d564ad0f2a10 100644
--- a/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java
+++ b/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java
@@ -23,7 +23,6 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
 import com.google.cloud.ByteArray;
 import com.google.cloud.Date;
@@ -56,7 +55,6 @@
 import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.internal.stubbing.answers.Returns;
 
 @RunWith(JUnit4.class)
 public class JdbcResultSetTest {
@@ -117,8 +115,7 @@ public class JdbcResultSetTest {
 
   private JdbcResultSet subject;
 
-  private static ResultSet getMockResultSet() {
-
+  static ResultSet getMockResultSet() {
     return ResultSets.forRows(
         Type.struct(
             StructField.of(STRING_COL_NULL, Type.string()),
@@ -186,190 +183,6 @@ private static ResultSet getMockResultSet() {
                 .build()));
   }
 
-  static ResultSet getMockResultSet_old() {
-    ResultSet res = mock(ResultSet.class);
-    when(res.getString(STRING_COL_NULL)).thenReturn(null);
-    when(res.isNull(STRING_COL_NULL)).thenReturn(true);
-    when(res.getString(STRING_COL_NOT_NULL)).thenReturn("FOO");
-    when(res.isNull(STRING_COL_NOT_NULL)).thenReturn(false);
-    when(res.getString(STRING_COLINDEX_NULL - 1)).thenReturn(null);
-    when(res.isNull(STRING_COLINDEX_NULL - 1)).thenReturn(true);
-    when(res.getString(STRING_COLINDEX_NOTNULL - 1)).thenReturn("BAR");
-    when(res.isNull(STRING_COLINDEX_NOTNULL - 1)).thenReturn(false);
-    when(res.getColumnType(STRING_COL_NULL)).thenReturn(Type.string());
-    when(res.getColumnType(STRING_COL_NOT_NULL)).thenReturn(Type.string());
-    when(res.getColumnType(STRING_COLINDEX_NULL - 1)).thenReturn(Type.string());
-    when(res.getColumnType(STRING_COLINDEX_NOTNULL - 1)).thenReturn(Type.string());
-
-    when(res.getString(URL_COL_NULL)).thenReturn(null);
-    when(res.isNull(URL_COL_NULL)).thenReturn(true);
-    when(res.getString(URL_COL_NOT_NULL)).thenReturn("https://github.com/");
-    when(res.isNull(URL_COL_NOT_NULL)).thenReturn(false);
-    when(res.getString(URL_COLINDEX_NULL - 1)).thenReturn(null);
-    when(res.isNull(URL_COLINDEX_NULL - 1)).thenReturn(true);
-    when(res.getString(URL_COLINDEX_NOTNULL - 1)).thenReturn("https://github.com/");
-    when(res.isNull(URL_COLINDEX_NOTNULL - 1)).thenReturn(false);
-    when(res.getColumnType(URL_COL_NULL)).thenReturn(Type.string());
-    when(res.getColumnType(URL_COL_NOT_NULL)).thenReturn(Type.string());
-    when(res.getColumnType(URL_COLINDEX_NULL - 1)).thenReturn(Type.string());
-    when(res.getColumnType(URL_COLINDEX_NOTNULL - 1)).thenReturn(Type.string());
-
-    when(res.getBoolean(BOOLEAN_COL_NULL)).thenReturn(false);
-    when(res.isNull(BOOLEAN_COL_NULL)).thenReturn(true);
-    when(res.getBoolean(BOOLEAN_COL_NOT_NULL)).thenReturn(true);
-    when(res.isNull(BOOLEAN_COL_NOT_NULL)).thenReturn(false);
-    when(res.getBoolean(BOOLEAN_COLINDEX_NULL - 1)).thenReturn(false);
-    when(res.isNull(BOOLEAN_COLINDEX_NULL - 1)).thenReturn(true);
-    when(res.getBoolean(BOOLEAN_COLINDEX_NOTNULL - 1)).thenReturn(false);
-    when(res.isNull(BOOLEAN_COLINDEX_NOTNULL - 1)).thenReturn(false);
-    when(res.getColumnType(BOOLEAN_COL_NULL)).thenReturn(Type.bool());
-    when(res.getColumnType(BOOLEAN_COL_NOT_NULL)).thenReturn(Type.bool());
-    when(res.getColumnType(BOOLEAN_COLINDEX_NULL - 1)).thenReturn(Type.bool());
-    when(res.getColumnType(BOOLEAN_COLINDEX_NOTNULL - 1)).thenReturn(Type.bool());
-
-    when(res.getDouble(DOUBLE_COL_NULL)).thenReturn(0d);
-    when(res.isNull(DOUBLE_COL_NULL)).thenReturn(true);
-    when(res.getDouble(DOUBLE_COL_NOT_NULL)).thenReturn(1.123456789d);
-    when(res.isNull(DOUBLE_COL_NOT_NULL)).thenReturn(false);
-    when(res.getDouble(DOUBLE_COLINDEX_NULL - 1)).thenReturn(0d);
-    when(res.isNull(DOUBLE_COLINDEX_NULL - 1)).thenReturn(true);
-    when(res.getDouble(DOUBLE_COLINDEX_NOTNULL - 1)).thenReturn(2.123456789d);
-    when(res.isNull(DOUBLE_COLINDEX_NOTNULL - 1)).thenReturn(false);
-    when(res.getColumnType(DOUBLE_COL_NULL)).thenReturn(Type.float64());
-    when(res.getColumnType(DOUBLE_COL_NOT_NULL)).thenReturn(Type.float64());
-    when(res.getColumnType(DOUBLE_COLINDEX_NULL - 1)).thenReturn(Type.float64());
-    when(res.getColumnType(DOUBLE_COLINDEX_NOTNULL - 1)).thenReturn(Type.float64());
-
-    when(res.getString(BYTES_COL_NULL)).thenReturn(null);
-    when(res.isNull(BYTES_COL_NULL)).thenReturn(true);
-    when(res.getBytes(BYTES_COL_NOT_NULL)).thenReturn(ByteArray.copyFrom("FOO"));
-    when(res.isNull(BYTES_COL_NOT_NULL)).thenReturn(false);
-    when(res.getBytes(BYTES_COLINDEX_NULL - 1)).thenReturn(null);
-    when(res.isNull(BYTES_COLINDEX_NULL - 1)).thenReturn(true);
-    when(res.getBytes(BYTES_COLINDEX_NOTNULL - 1)).thenReturn(ByteArray.copyFrom("BAR"));
-    when(res.isNull(BYTES_COLINDEX_NOTNULL - 1)).thenReturn(false);
-    when(res.getColumnType(BYTES_COL_NULL)).thenReturn(Type.bytes());
-    when(res.getColumnType(BYTES_COL_NOT_NULL)).thenReturn(Type.bytes());
-    when(res.getColumnType(BYTES_COLINDEX_NULL - 1)).thenReturn(Type.bytes());
-    when(res.getColumnType(BYTES_COLINDEX_NOTNULL - 1)).thenReturn(Type.bytes());
-
-    when(res.getLong(LONG_COL_NULL)).thenReturn(0l);
-    when(res.isNull(LONG_COL_NULL)).thenReturn(true);
-    when(res.getLong(LONG_COL_NOT_NULL)).thenReturn(1l);
-    when(res.isNull(LONG_COL_NOT_NULL)).thenReturn(false);
-    when(res.getLong(LONG_COLINDEX_NULL - 1)).thenReturn(0l);
-    when(res.isNull(LONG_COLINDEX_NULL - 1)).thenReturn(true);
-    when(res.getLong(LONG_COLINDEX_NOTNULL - 1)).thenReturn(2l);
-    when(res.isNull(LONG_COLINDEX_NOTNULL - 1)).thenReturn(false);
-    when(res.getColumnType(LONG_COL_NULL)).thenReturn(Type.int64());
-    when(res.getColumnType(LONG_COL_NOT_NULL)).thenReturn(Type.int64());
-    when(res.getColumnType(LONG_COLINDEX_NULL - 1)).thenReturn(Type.int64());
-    when(res.getColumnType(LONG_COLINDEX_NOTNULL - 1)).thenReturn(Type.int64());
-
-    when(res.getDate(DATE_COL_NULL)).thenAnswer(new Returns(null));
-    when(res.isNull(DATE_COL_NULL)).thenAnswer(new Returns(true));
-    when(res.getDate(DATE_COL_NOT_NULL))
-        .thenAnswer(new Returns(Date.fromYearMonthDay(2017, 9, 10)));
-    when(res.isNull(DATE_COL_NOT_NULL)).thenAnswer(new Returns(false));
-    when(res.getDate(DATE_COLINDEX_NULL - 1)).thenAnswer(new Returns(null));
-    when(res.isNull(DATE_COLINDEX_NULL - 1)).thenAnswer(new Returns(true));
-    when(res.getDate(DATE_COLINDEX_NOTNULL - 1))
-        .thenAnswer(new Returns(Date.fromYearMonthDay(2017, 9, 10)));
-    when(res.isNull(DATE_COLINDEX_NOTNULL - 1)).thenAnswer(new Returns(false));
-    when(res.getColumnType(DATE_COL_NULL)).thenAnswer(new Returns(Type.date()));
-    when(res.getColumnType(DATE_COL_NOT_NULL)).thenAnswer(new Returns(Type.date()));
-    when(res.getColumnType(DATE_COLINDEX_NULL - 1)).thenAnswer(new Returns(Type.date()));
-    when(res.getColumnType(DATE_COLINDEX_NOTNULL - 1)).thenAnswer(new Returns(Type.date()));
-
-    Calendar cal1 = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
-    cal1.clear();
-    cal1.set(2017, 8, 10, 8, 15, 59);
-    Calendar cal2 = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
-    cal2.clear();
-    cal2.set(2017, 8, 11, 8, 15, 59);
-    when(res.getTimestamp(TIMESTAMP_COL_NULL)).thenReturn(null);
-    when(res.isNull(TIMESTAMP_COL_NULL)).thenReturn(true);
-    when(res.getTimestamp(TIMESTAMP_COL_NOT_NULL)).thenReturn(Timestamp.of(cal1.getTime()));
-    when(res.isNull(TIMESTAMP_COL_NOT_NULL)).thenReturn(false);
-    when(res.getTimestamp(TIMESTAMP_COLINDEX_NULL - 1)).thenReturn(null);
-    when(res.isNull(TIMESTAMP_COLINDEX_NULL - 1)).thenReturn(true);
-    when(res.getTimestamp(TIMESTAMP_COLINDEX_NOTNULL - 1)).thenReturn(Timestamp.of(cal2.getTime()));
-    when(res.isNull(TIMESTAMP_COLINDEX_NOTNULL - 1)).thenReturn(false);
-    when(res.getColumnType(TIMESTAMP_COL_NULL)).thenReturn(Type.timestamp());
-    when(res.getColumnType(TIMESTAMP_COL_NOT_NULL)).thenReturn(Type.timestamp());
-    when(res.getColumnType(TIMESTAMP_COLINDEX_NULL - 1)).thenReturn(Type.timestamp());
-    when(res.getColumnType(TIMESTAMP_COLINDEX_NOTNULL - 1)).thenReturn(Type.timestamp());
-
-    Calendar cal3 = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
-    cal3.clear();
-    cal3.set(1970, 0, 1, 14, 6, 15);
-    Calendar cal4 = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
-    cal4.clear();
-    cal4.set(1970, 0, 1, 14, 6, 15);
-    when(res.getTimestamp(TIME_COL_NULL)).thenReturn(null);
-    when(res.isNull(TIME_COL_NULL)).thenReturn(true);
-    when(res.getTimestamp(TIME_COL_NOT_NULL)).thenReturn(Timestamp.of(cal3.getTime()));
-    when(res.isNull(TIME_COL_NOT_NULL)).thenReturn(false);
-    when(res.getTimestamp(TIME_COLINDEX_NULL - 1)).thenReturn(null);
-    when(res.isNull(TIME_COLINDEX_NULL - 1)).thenReturn(true);
-    when(res.getTimestamp(TIME_COLINDEX_NOTNULL - 1)).thenReturn(Timestamp.of(cal4.getTime()));
-    when(res.isNull(TIME_COLINDEX_NOTNULL - 1)).thenReturn(false);
-    when(res.getColumnType(TIME_COL_NULL)).thenReturn(Type.timestamp());
-    when(res.getColumnType(TIME_COL_NOT_NULL)).thenReturn(Type.timestamp());
-    when(res.getColumnType(TIME_COLINDEX_NULL - 1)).thenReturn(Type.timestamp());
-    when(res.getColumnType(TIME_COLINDEX_NOTNULL - 1)).thenReturn(Type.timestamp());
-
-    when(res.getLongList(ARRAY_COL_NULL)).thenAnswer(new Returns(null));
-    when(res.isNull(ARRAY_COL_NULL)).thenAnswer(new Returns(true));
-    when(res.getLongList(ARRAY_COL_NOT_NULL)).thenAnswer(new Returns(Arrays.asList(1L, 2L, 3L)));
-    when(res.isNull(ARRAY_COL_NOT_NULL)).thenAnswer(new Returns(false));
-    when(res.getLongList(ARRAY_COLINDEX_NULL - 1)).thenAnswer(new Returns(null));
-    when(res.isNull(ARRAY_COLINDEX_NULL - 1)).thenAnswer(new Returns(true));
-    when(res.getLongList(ARRAY_COLINDEX_NOTNULL - 1))
-        .thenAnswer(new Returns(Arrays.asList(1L, 2L, 3L)));
-    when(res.isNull(ARRAY_COLINDEX_NOTNULL - 1)).thenAnswer(new Returns(false));
-    when(res.getColumnType(ARRAY_COL_NULL)).thenAnswer(new Returns(Type.array(Type.int64())));
-    when(res.getColumnType(ARRAY_COL_NOT_NULL)).thenAnswer(new Returns(Type.array(Type.int64())));
-    when(res.getColumnType(ARRAY_COLINDEX_NULL - 1))
-        .thenAnswer(new Returns(Type.array(Type.int64())));
-    when(res.getColumnType(ARRAY_COLINDEX_NOTNULL - 1))
-        .thenAnswer(new Returns(Type.array(Type.int64())));
-
-    when(res.getColumnIndex(STRING_COL_NOT_NULL)).thenAnswer(new Returns(1));
-    when(res.getColumnIndex(UNKNOWN_COLUMN)).thenThrow(new IllegalArgumentException());
-    when(res.getColumnIndex(DATE_COL_NOT_NULL)).thenAnswer(new Returns(DATE_COLINDEX_NOTNULL - 1));
-    when(res.getColumnIndex(ARRAY_COL_NOT_NULL))
-        .thenAnswer(new Returns(ARRAY_COLINDEX_NOTNULL - 1));
-    when(res.getColumnIndex(ARRAY_COL_NULL)).thenAnswer(new Returns(ARRAY_COLINDEX_NULL - 1));
-
-    when(res.getType())
-        .thenReturn(
-            Type.struct(
-                StructField.of(STRING_COL_NULL, Type.string()),
-                StructField.of(STRING_COL_NOT_NULL, Type.string()),
-                StructField.of(BOOLEAN_COL_NULL, Type.bool()),
-                StructField.of(BOOLEAN_COL_NOT_NULL, Type.bool()),
-                StructField.of(DOUBLE_COL_NULL, Type.float64()),
-                StructField.of(DOUBLE_COL_NOT_NULL, Type.float64()),
-                StructField.of(BYTES_COL_NULL, Type.bytes()),
-                StructField.of(BYTES_COL_NOT_NULL, Type.bytes()),
-                StructField.of(LONG_COL_NULL, Type.int64()),
-                StructField.of(LONG_COL_NOT_NULL, Type.int64()),
-                StructField.of(DATE_COL_NULL, Type.date()),
-                StructField.of(DATE_COL_NOT_NULL, Type.date()),
-                StructField.of(TIMESTAMP_COL_NULL, Type.timestamp()),
-                StructField.of(TIMESTAMP_COL_NOT_NULL, Type.timestamp()),
-                StructField.of(TIME_COL_NULL, Type.timestamp()),
-                StructField.of(TIME_COL_NOT_NULL, Type.timestamp()),
-                StructField.of(URL_COL_NULL, Type.string()),
-                StructField.of(URL_COL_NOT_NULL, Type.string())));
-
-    // Next behaviour.
-    when(res.next()).thenReturn(true, true, true, true, false);
-
-    return res;
-  }
-
   public JdbcResultSetTest() throws SQLException {
     subject = JdbcResultSet.of(mock(Statement.class), getMockResultSet());
     subject.next();
