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();
+ }
+
+ }
+}