Add regression tests for JDBCAccess

Add regression tests for JDBCAccess to have a safety net for
refactoring. The tests stub a JDBC PreparedStatement to assert
correct behaviour of JDBCAccess depending on the outcome of the
statement execution.

Change-Id: I31713863e97cb50719118950b5c1a25a2620134f
Signed-off-by: Adrian Goerler <adrian.goerler@sap.com>
diff --git a/pom.xml b/pom.xml
index 28ae85a..39c4468 100644
--- a/pom.xml
+++ b/pom.xml
@@ -325,6 +325,13 @@
     </dependency>
 
     <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-all</artifactId>
+      <version>1.8.4</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
       <groupId>com.h2database</groupId>
       <artifactId>h2</artifactId>
       <version>1.2.125</version>
diff --git a/src/test/java/com/google/gwtorm/jdbc/TestJdbcAccess.java b/src/test/java/com/google/gwtorm/jdbc/TestJdbcAccess.java
new file mode 100644
index 0000000..096a531
--- /dev/null
+++ b/src/test/java/com/google/gwtorm/jdbc/TestJdbcAccess.java
@@ -0,0 +1,485 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// 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.gwtorm.jdbc;
+
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import com.google.gwtorm.client.Key;
+import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmConcurrencyException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.stubbing.OngoingStubbing;
+
+import java.sql.BatchUpdateException;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.HashSet;
+import java.util.Set;
+
+public class TestJdbcAccess {
+
+  private static final String INSERT = "insert";
+  private static final String UPDATE = "update";
+  private static final String DELETE = "delete";
+
+  private static final SqlDialect DIALECT = mock(SqlDialect.class,
+      CALLS_REAL_METHODS);
+
+  private static final Iterable<Data> NO_DATA = asList();
+  private static  Iterable<Data> ONE_ROW = asList(new Data(1));
+  private static  Iterable<Data> TWO_ROWS = asList(new Data(1), new Data(2));
+  private Connection conn;
+
+
+  private PreparedStatement stubStatementWithUpdateCounts(String command,
+      final int... updateCounts) throws SQLException {
+    PreparedStatement ps = mock(PreparedStatement.class);
+
+    // batching
+    doNothing().when(ps).addBatch();
+    when(ps.executeBatch()).thenReturn(updateCounts);
+
+    // non-batching
+    if (updateCounts != null && updateCounts.length > 0) {
+      OngoingStubbing<Integer> stubber = when(ps.executeUpdate());
+      for (int updateCount : updateCounts) {
+        stubber = stubber.thenReturn(updateCount);
+      }
+    }
+
+    when(conn.prepareStatement(command)).thenReturn(ps);
+    return ps;
+  }
+
+  private PreparedStatement stubStatementThrowExceptionOnExecute(
+      String command, SQLException exception) throws SQLException {
+    PreparedStatement ps = mock(PreparedStatement.class);
+    doNothing().when(ps).addBatch();
+    when(ps.executeBatch()).thenThrow(exception);
+    when(ps.executeUpdate()).thenThrow(exception);
+    when(conn.prepareStatement(command)).thenReturn(ps);
+    return ps;
+  }
+
+  private JdbcAccess<Data, Data.DataKey> createJdbcAccess(
+      final SqlDialect dialect) {
+    JdbcSchema schema = setupSchema(dialect);
+
+    JdbcAccess<Data, Data.DataKey> classUnderTest = new DataJdbcAccess(schema);
+    return classUnderTest;
+  }
+
+  private JdbcAccess<Data, Data.DataKey> createClassUnderTest() {
+    return createJdbcAccess(DIALECT);
+  }
+
+  private JdbcSchema setupSchema(final SqlDialect dialect) {
+    @SuppressWarnings("rawtypes")
+    Database db = mock(Database.class);
+    try {
+      when(db.getDialect()).thenReturn(dialect);
+
+      when(db.newConnection()).thenReturn(conn);
+
+      JdbcSchema schema = new Schema(db);
+      return schema;
+    } catch (OrmException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private static void assertNotUsed(PreparedStatement insert) {
+    verifyZeroInteractions(insert);
+  }
+
+  private static void assertUsedBatchingOnly(PreparedStatement statement)
+      throws SQLException {
+    verify(statement, atLeastOnce()).addBatch();
+    verify(statement).executeBatch();
+    verify(statement, never()).executeUpdate();
+  }
+
+  private static void assertExpectedIdsUsed(PreparedStatement statement,
+      int... ids) throws SQLException {
+
+    Set<Integer> notSet = new HashSet<Integer>(2);
+    notSet.add(1);
+    notSet.add(2);
+
+    for (int id : ids) {
+      verify(statement).setInt(1, id);
+      notSet.remove(Integer.valueOf(id));
+    }
+
+    for (Integer id : notSet) {
+      verify(statement, never()).setInt(1, id);
+    }
+  }
+
+  @Before
+  public void setup() {
+    conn = mock(Connection.class);
+  }
+
+  @Test
+  public void testInsertNothing() throws OrmException {
+    setup();
+    createClassUnderTest().insert(NO_DATA);
+  }
+
+  @Test
+  public void testInsertOne() throws OrmException, SQLException {
+    PreparedStatement insert = stubStatementWithUpdateCounts(INSERT, 1);
+
+    createClassUnderTest().insert(ONE_ROW);
+
+    assertUsedBatchingOnly(insert);
+  }
+
+  @Test
+  public void testInsertOneDBException() throws OrmException, SQLException {
+    SQLException exception = new BatchUpdateException();
+    PreparedStatement insert =
+        stubStatementThrowExceptionOnExecute(INSERT, exception);
+    JdbcAccess<Data, Data.DataKey> classUnderTest = createClassUnderTest();
+    try {
+      classUnderTest.insert(ONE_ROW);
+      fail("missingException");
+    } catch (OrmException e) {
+      // expected
+      assertSame(e.getCause(), exception);
+    }
+
+    assertUsedBatchingOnly(insert);
+  }
+
+  @Test
+  public void testUpdateNothing() throws OrmException {
+    createClassUnderTest().update(NO_DATA);
+  }
+
+  @Test
+  public void testUpdateOne() throws OrmException, SQLException {
+    PreparedStatement update = stubStatementWithUpdateCounts(UPDATE, 1);
+
+    createClassUnderTest().update(ONE_ROW);
+
+    assertUsedBatchingOnly(update);
+  }
+
+  @Test
+  public void testUpdateOneDBException() throws OrmException, SQLException {
+    SQLException exception = new BatchUpdateException();
+    PreparedStatement update =
+        stubStatementThrowExceptionOnExecute(UPDATE, exception);
+    JdbcAccess<Data, Data.DataKey> classUnderTest = createClassUnderTest();
+    try {
+      classUnderTest.update(ONE_ROW);
+      fail("missingException");
+    } catch (OrmException e) {
+      // expected
+      assertSame(e.getCause(), exception);
+    }
+
+    assertUsedBatchingOnly(update);
+  }
+
+  @Test
+  public void testUpdateOneConcurrentlyModifiedException() throws SQLException,
+      OrmException {
+    PreparedStatement update = stubStatementWithUpdateCounts(UPDATE, 0);
+    JdbcAccess<Data, Data.DataKey> classUnderTest = createClassUnderTest();
+    try {
+      classUnderTest.update(ONE_ROW);
+      fail("missing OrmConcurrencyException");
+    } catch (OrmConcurrencyException e) {
+      // expected
+    }
+    assertUsedBatchingOnly(update);
+  }
+
+  @Test
+  public void testUpsertNothing() throws OrmException, SQLException {
+    createClassUnderTest().upsert(NO_DATA);
+  }
+
+  @Test
+  public void testUpsertOneExisting() throws OrmException, SQLException {
+    PreparedStatement update = stubStatementWithUpdateCounts(UPDATE, 1);
+    PreparedStatement insert = stubStatementWithUpdateCounts(INSERT);
+
+    createClassUnderTest().upsert(ONE_ROW);
+
+    assertUsedBatchingOnly(update);
+    assertNotUsed(insert);
+  }
+
+  @Test
+  public void testUpsertOneException() throws OrmException, SQLException {
+    SQLException exception = new BatchUpdateException();
+    PreparedStatement update =
+        stubStatementThrowExceptionOnExecute(UPDATE, exception);
+    JdbcAccess<Data, Data.DataKey> classUnderTest = createClassUnderTest();
+    try {
+      classUnderTest.upsert(ONE_ROW);
+      fail("missingException");
+    } catch (OrmException e) {
+      // expected
+      assertSame(e.getCause(), exception);
+    }
+
+    assertUsedBatchingOnly(update);
+  }
+
+  @Test
+  public void testUpsertOneNotExisting() throws OrmException, SQLException {
+    PreparedStatement update = stubStatementWithUpdateCounts(UPDATE);
+    PreparedStatement insert = stubStatementWithUpdateCounts(INSERT, 1);
+
+    createClassUnderTest().upsert(ONE_ROW);
+
+    assertUsedBatchingOnly(update);
+    assertUsedBatchingOnly(insert);
+    assertExpectedIdsUsed(insert, 1);
+  }
+
+  @Test
+  public void testUpsertTwoNotExistingZeroLengthArray() throws SQLException,
+      OrmException {
+    PreparedStatement update = stubStatementWithUpdateCounts(UPDATE);
+    PreparedStatement insert = stubStatementWithUpdateCounts(INSERT, 1, 1);
+
+    createClassUnderTest().upsert(TWO_ROWS);
+
+    assertUsedBatchingOnly(update);
+    assertUsedBatchingOnly(insert);
+    assertExpectedIdsUsed(insert, 1, 2);
+  }
+
+  @Test
+  public void testUpsertTwoNotExisting() throws SQLException, OrmException {
+    PreparedStatement update = stubStatementWithUpdateCounts(UPDATE, 0, 0);
+    PreparedStatement insert = stubStatementWithUpdateCounts(INSERT, 1, 1);
+
+    createClassUnderTest().upsert(TWO_ROWS);
+
+    assertUsedBatchingOnly(update);
+    assertUsedBatchingOnly(insert);
+    assertExpectedIdsUsed(insert, 1, 2);
+  }
+
+  @Test
+  public void testUpsertTwoBothExisting() throws SQLException, OrmException {
+    PreparedStatement update = stubStatementWithUpdateCounts(UPDATE, 1, 1);
+    PreparedStatement insert = stubStatementWithUpdateCounts(INSERT);
+
+    createClassUnderTest().upsert(TWO_ROWS);
+
+    assertUsedBatchingOnly(update);
+    assertNotUsed(insert);
+  }
+
+  @Test
+  public void testUpsertTwoFirstExisting() throws SQLException, OrmException {
+    PreparedStatement update = stubStatementWithUpdateCounts(UPDATE, 1, 0);
+    PreparedStatement insert = stubStatementWithUpdateCounts(INSERT, 1);
+
+    createClassUnderTest().upsert(TWO_ROWS);
+
+    assertUsedBatchingOnly(update);
+    assertUsedBatchingOnly(insert);
+    assertExpectedIdsUsed(insert, 2);
+  }
+
+  @Test
+  public void testUpsertTwoSecondExisting() throws SQLException, OrmException {
+    PreparedStatement update = stubStatementWithUpdateCounts(UPDATE, 0, 1);
+    PreparedStatement insert = stubStatementWithUpdateCounts(INSERT, 1);
+
+    createClassUnderTest().upsert(TWO_ROWS);
+
+    assertUsedBatchingOnly(update);
+    assertUsedBatchingOnly(insert);
+    assertExpectedIdsUsed(insert, 1);
+  }
+
+  @Test
+  public void testUpsertTwoUpdateCountsAreNull() throws SQLException,
+      OrmException {
+    PreparedStatement update = stubStatementWithUpdateCounts(UPDATE, null);
+    PreparedStatement insert = stubStatementWithUpdateCounts(INSERT, 1, 1);
+
+    createClassUnderTest().upsert(TWO_ROWS);
+
+    assertUsedBatchingOnly(update);
+    assertUsedBatchingOnly(insert);
+    assertExpectedIdsUsed(insert, 1, 2);
+  }
+
+  @Test
+  public void testDeleteOneExisting() throws SQLException, OrmException {
+    PreparedStatement delete = stubStatementWithUpdateCounts(DELETE, 1);
+
+    createClassUnderTest().delete(ONE_ROW);
+
+    assertUsedBatchingOnly(delete);
+  }
+
+  @Test
+  public void testDeleteOneNotExisting() throws SQLException, OrmException {
+    PreparedStatement delete = stubStatementWithUpdateCounts(DELETE, 0);
+    JdbcAccess<Data, Data.DataKey> classUnderTest = createClassUnderTest();
+    try {
+      classUnderTest.delete(ONE_ROW);
+      fail("missing OrmConcurrencyException");
+    } catch (OrmConcurrencyException e) {
+      // expected
+    }
+
+    assertUsedBatchingOnly(delete);
+  }
+
+  private class Schema extends JdbcSchema {
+
+    protected Schema(Database<?> d) throws OrmException {
+      super(d);
+    }
+
+    @Override
+    public Access<?, ?>[] allRelations() {
+      throw new UnsupportedOperationException();
+    }
+
+  }
+
+  private static class Data {
+
+    private final int id;
+
+    Data(int anId) {
+      id = anId;
+    }
+
+    private static class DataKey implements Key<Key<?>> {
+
+      @Override
+      public com.google.gwtorm.client.Key<?> getParentKey() {
+        throw new UnsupportedOperationException();
+      }
+
+      @Override
+      public void fromString(String in) {
+        throw new UnsupportedOperationException();
+      }
+
+    }
+
+  }
+
+  private static class DataJdbcAccess extends JdbcAccess<Data, Data.DataKey> {
+
+    protected DataJdbcAccess(JdbcSchema s) {
+      super(s);
+    }
+
+    public String getRelationName() {
+      return "Data";
+    }
+
+    @Override
+    public com.google.gwtorm.jdbc.TestJdbcAccess.Data.DataKey primaryKey(
+        Data entity) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Data get(com.google.gwtorm.jdbc.TestJdbcAccess.Data.DataKey key)
+        throws OrmException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected Data newEntityInstance() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected String getInsertOneSql() {
+      return INSERT;
+    }
+
+    @Override
+    protected String getUpdateOneSql() {
+      return UPDATE;
+    }
+
+    @Override
+    protected String getDeleteOneSql() {
+      return DELETE;
+    }
+
+    @Override
+    protected void bindOneInsert(PreparedStatement ps, Data entity)
+        throws SQLException {
+      ps.setInt(1, entity.id);
+    }
+
+    @Override
+    protected void bindOneUpdate(PreparedStatement ps, Data entity)
+        throws SQLException {
+      ps.setInt(1, entity.id);
+    }
+
+    @Override
+    protected void bindOneDelete(PreparedStatement ps, Data entity)
+        throws SQLException {
+      ps.setInt(1, entity.id);
+    }
+
+    @Override
+    protected void bindOneFetch(java.sql.ResultSet rs, Data entity)
+        throws SQLException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getRelationID() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ResultSet<Data> iterateAllEntities() throws OrmException {
+      throw new UnsupportedOperationException();
+    }
+
+  }
+}