refactor migration applier, added tests

This commit is contained in:
wea_ondara
2022-11-26 05:35:59 +01:00
parent 88366c937a
commit 913df99732
2 changed files with 392 additions and 23 deletions

View File

@@ -12,7 +12,9 @@ import lombok.Getter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -25,22 +27,19 @@ public abstract class MigrationApplier {
protected final SqlPlatform sqlPlatform;
public void migrate() throws MigrationException {
var migrations = findMigrations(options.getMigrationsPackage()); //TODO find all migrations, support multiple db contexts
var migrations = new ArrayList<>(findMigrations(options.getMigrationsPackage())); //TODO find all migrations, support multiple db contexts
try {
if (migrationsTableExists()) {
getAppliedMigrations().forEach(a -> migrations.removeIf(m -> m.getClass().getSimpleName().equals(a)));
} else {
createMigrationsTable();
}
ensureMigrationsTableExists();
getAppliedMigrations().forEach(a -> migrations.removeIf(m -> m.getClass().getSimpleName().equals(a)));
for (Migration migration : migrations) {
applyMigration(migration);
}
} catch (SQLException e) {
throw new MigrationException(e);
throw new MigrationException("Error while applying migrations: " + e.getLocalizedMessage(), e);
}
}
protected void applyMigration(Migration m) throws MigrationException {
protected void applyMigration(Migration m) throws MigrationException {//TODO verify transactions work
var mb = new MigrationBuilder();
m.up(mb);
var operations = mb.getOperations().stream().map(MigrationOperation.Builder::build).toList();
@@ -49,17 +48,7 @@ public abstract class MigrationApplier {
connection.setAutoCommit(false);
try {
for (MigrationOperation op : operations) {
var stmt = sqlPlatform.getTranslator().translate(connection, op);
try {
stmt.execute();
} catch (SQLException e) {
throw new SQLException("Failed to execute query: " + stmt, e);
} finally {
try {
stmt.close();
} catch (SQLException ignored) {
}
}
applyMigrationOperation(op);
}
insertMigrationLog(m);
connection.commit();
@@ -74,7 +63,26 @@ public abstract class MigrationApplier {
}
}
protected List<Migration> findMigrations(String packageName) throws MigrationException {
protected void applyMigrationOperation(MigrationOperation operation) throws SQLException {
PreparedStatement stmt;
try {
stmt = sqlPlatform.getTranslator().translate(connection, operation);//TODO may return an object containing the prep stmt and a string rep of the query
} catch (SQLException e) {
throw new SQLException("Failed to build query: " + e.getLocalizedMessage(), e);
}
try {
stmt.execute();
} catch (SQLException e) {
throw new SQLException("Failed to execute query: " + stmt, e);
} finally {
try {
stmt.close();
} catch (SQLException ignored) {
}
}
}
protected List<Migration> findMigrations(String packageName) throws MigrationException {//TODO rewrite this function after adding annotations to migrations (@Context, @Name, ...), also get rid of the packageName restristrict and just read the classfiles with asm
try (var is = getClass().getClassLoader().getResourceAsStream(packageName.replace(".", "/"));
var reader = new BufferedReader(new InputStreamReader(is))) {
return reader.lines()
@@ -101,11 +109,21 @@ public abstract class MigrationApplier {
}
}
protected void ensureMigrationsTableExists() throws MigrationException {
try {
if (!migrationsTableExists()) {
createMigrationsTable();
}
} catch (SQLException e) {
throw new MigrationException("Failed to create migrations log table", e);
}
}
protected abstract boolean migrationsTableExists() throws SQLException;
protected abstract void createMigrationsTable() throws SQLException;
protected abstract void createMigrationsTable() throws SQLException;//TODO use a hardcoded migration
protected abstract List<String> getAppliedMigrations() throws SQLException;
protected abstract List<String> getAppliedMigrations() throws SQLException;//TODO dbset to load later
protected abstract void insertMigrationLog(Migration m) throws SQLException;
protected abstract void insertMigrationLog(Migration m) throws SQLException;//TODO dbset to insert later
}

View File

@@ -0,0 +1,351 @@
package jef.platform.base.migration;
import jef.MigrationException;
import jef.model.migration.Migration;
import jef.model.migration.MigrationBuilder;
import jef.model.migration.operation.MigrationOperation;
import jef.platform.SqlPlatform;
import jef.platform.base.DatabaseOptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.withSettings;
class MigrationApplierTest {
private String packageName;
private DatabaseOptions options;
private Connection connection;
private SqlPlatform platform;
private MigrationApplier applier;
private Object[] allMocks;
@BeforeEach
void setup() {
//setup
packageName = "package";
options = mock(DatabaseOptions.class);
doReturn(packageName).when(options).getMigrationsPackage();
connection = mock(Connection.class);
platform = mock(SqlPlatform.class);
applier = mock(MigrationApplier.class, withSettings().useConstructor(connection, options, platform).defaultAnswer(CALLS_REAL_METHODS)); //spy
allMocks = List.of(options, connection, platform, applier).toArray();
}
@Test
void migrate_success() throws MigrationException, SQLException {
interface Migration1 extends Migration {
}
interface Migration2 extends Migration {
}
var migration1 = mock(Migration1.class);
var migration2 = mock(Migration2.class);
doReturn(List.of(migration1, migration2)).when(applier).findMigrations(packageName);
doNothing().when(applier).ensureMigrationsTableExists();
doReturn(List.of(migration1.getClass().getSimpleName())).when(applier).getAppliedMigrations();
doNothing().when(applier).applyMigration(any(Migration.class));
//test
applier.migrate();
//assert
verify(applier, times(1)).migrate();
verify(options, times(1)).getMigrationsPackage();
verify(applier, times(1)).findMigrations(packageName);
verify(applier, times(1)).ensureMigrationsTableExists();
verify(applier, times(1)).getAppliedMigrations();
verify(applier, times(1)).applyMigration(migration2);
verifyNoMoreInteractions(migration1, migration2);
verifyNoMoreInteractions(allMocks);
}
@Test
void migrate_findMigrationsThrowsMigrationException() throws MigrationException {
//setup
var exception = new MigrationException("reason");
doThrow(exception).when(applier).findMigrations(packageName);
//test
var ex = assertThrows(MigrationException.class, () -> applier.migrate());
//assert
assertSame(exception, ex);
verify(applier, times(1)).migrate();
verify(options, times(1)).getMigrationsPackage();
verify(applier, times(1)).findMigrations(packageName);
verifyNoMoreInteractions(allMocks);
}
@Test
void migrate_ensureMigrationsTableExistsThrowsMigrationException() throws MigrationException {
//setup
doReturn(List.of()).when(applier).findMigrations(packageName);
var exception = new MigrationException("reason");
doThrow(exception).when(applier).ensureMigrationsTableExists();
//test
var ex = assertThrows(MigrationException.class, () -> applier.migrate());
//assert
assertSame(exception, ex);
verify(applier, times(1)).migrate();
verify(options, times(1)).getMigrationsPackage();
verify(applier, times(1)).findMigrations(packageName);
verify(applier, times(1)).ensureMigrationsTableExists();
verifyNoMoreInteractions(allMocks);
}
@Test
void migrate_getAppliedMigrationsThrowsSqlException() throws MigrationException, SQLException {
//setup
doReturn(List.of()).when(applier).findMigrations(packageName);
doNothing().when(applier).ensureMigrationsTableExists();
var exception = new SQLException("reason");
doThrow(exception).when(applier).getAppliedMigrations();
//test
var ex = assertThrows(MigrationException.class, () -> applier.migrate());
//assert
assertEquals("Error while applying migrations: reason", ex.getMessage());
assertSame(exception, ex.getCause());
verify(applier, times(1)).migrate();
verify(options, times(1)).getMigrationsPackage();
verify(applier, times(1)).findMigrations(packageName);
verify(applier, times(1)).ensureMigrationsTableExists();
verify(applier, times(1)).getAppliedMigrations();
verifyNoMoreInteractions(allMocks);
}
@Test
void migrate_applyMigrationThrowsMigrationException() throws MigrationException, SQLException {
//setup
var migration = mock(Migration.class);
doReturn(List.of(migration)).when(applier).findMigrations(packageName);
doNothing().when(applier).ensureMigrationsTableExists();
doReturn(List.of()).when(applier).getAppliedMigrations();
var exception = new MigrationException("reason");
doThrow(exception).when(applier).applyMigration(migration);
//test
var ex = assertThrows(MigrationException.class, () -> applier.migrate());
//assert
assertSame(exception, ex);
verify(applier, times(1)).migrate();
verify(options, times(1)).getMigrationsPackage();
verify(applier, times(1)).findMigrations(packageName);
verify(applier, times(1)).ensureMigrationsTableExists();
verify(applier, times(1)).getAppliedMigrations();
verify(applier, times(1)).applyMigration(migration);
verifyNoMoreInteractions(migration);
verifyNoMoreInteractions(allMocks);
}
@Test
void applyMigration_success() throws SQLException, MigrationException {
//setup
var migration = mock(Migration.class);
var operationBuilder = new MigrationOperation.Builder[]{null};
var operation = new MigrationOperation[]{null};
doAnswer(invok -> {
operationBuilder[0] = ((MigrationBuilder) invok.getArguments()[0]).addTable("test", List.of());
operation[0] = operationBuilder[0].build();
return null;
}).when(migration).up(any(MigrationBuilder.class));
doNothing().when(applier).applyMigrationOperation(argThat(e -> e.equals(operation[0])));//eq(operation[0]) does not work as var would have to be set already and isn't
//test
applier.applyMigration(migration);
//assert
verify(applier, times(1)).applyMigration(migration);
verify(migration, times(1)).up(any(MigrationBuilder.class));
verify(connection, times(1)).setAutoCommit(false);
verify(applier, times(1)).applyMigrationOperation(operation[0]);
verify(applier, times(1)).insertMigrationLog(migration);
verify(connection, times(1)).commit();
verify(connection, times(1)).setAutoCommit(true);
verifyNoMoreInteractions(migration);
verifyNoMoreInteractions(allMocks);
}
@Test
void applyMigration_applyMigrationOperationThrowsSqlException() throws SQLException, MigrationException {
//setup
var migration = mock(Migration.class);
var operationBuilder = new MigrationOperation.Builder[]{null};
var operation = new MigrationOperation[]{null};
doAnswer(invok -> {
operationBuilder[0] = ((MigrationBuilder) invok.getArguments()[0]).addTable("test", List.of());
operation[0] = operationBuilder[0].build();
return null;
}).when(migration).up(any(MigrationBuilder.class));
var sqlException = new SQLException("reason");
doThrow(sqlException).when(applier).applyMigrationOperation(argThat(e -> e.equals(operation[0])));//eq(operation[0]) does not work as var would have to be set already and isn't
//test
var thrown = assertThrows(MigrationException.class, () -> applier.applyMigration(migration));
//assert
assertTrue(thrown.getMessage().matches("Failed to apply migration '(.*?)' to the database: reason"));
assertSame(sqlException, thrown.getCause());
verify(applier, times(1)).applyMigration(migration);
verify(migration, times(1)).up(any(MigrationBuilder.class));
verify(connection, times(1)).setAutoCommit(false);
verify(applier, times(1)).applyMigrationOperation(operation[0]);
verify(connection, times(1)).rollback();
verify(connection, times(1)).setAutoCommit(true);
verifyNoMoreInteractions(migration);
verifyNoMoreInteractions(allMocks);
}
@Test
void applyMigration_insertMigrationLogThrowsSqlException() throws SQLException, MigrationException {
//setup
var migration = mock(Migration.class);
var operationBuilder = new MigrationOperation.Builder[]{null};
var operation = new MigrationOperation[]{null};
doAnswer(invok -> {
operationBuilder[0] = ((MigrationBuilder) invok.getArguments()[0]).addTable("test", List.of());
operation[0] = operationBuilder[0].build();
return null;
}).when(migration).up(any(MigrationBuilder.class));
var sqlException = new SQLException("reason");
doNothing().when(applier).applyMigrationOperation(argThat(e -> e.equals(operation[0])));//eq(operation[0]) does not work as var would have to be set already and isn't
doThrow(sqlException).when(applier).insertMigrationLog(migration);
//test
var thrown = assertThrows(MigrationException.class, () -> applier.applyMigration(migration));
//assert
assertTrue(thrown.getMessage().matches("Failed to apply migration '(.*?)' to the database: reason"));
assertSame(sqlException, thrown.getCause());
verify(applier, times(1)).applyMigration(migration);
verify(migration, times(1)).up(any(MigrationBuilder.class));
verify(connection, times(1)).setAutoCommit(false);
verify(applier, times(1)).applyMigrationOperation(operation[0]);
verify(applier, times(1)).insertMigrationLog(migration);
verify(connection, times(1)).rollback();
verify(connection, times(1)).setAutoCommit(true);
verifyNoMoreInteractions(migration);
verifyNoMoreInteractions(allMocks);
}
@Test
void applyMigrationOperation_success() throws SQLException {
//setup
var translator = mock(MigrationOperationTranslator.class);
doReturn(translator).when(platform).getTranslator();
var operation = mock(MigrationOperation.class);
var stmt = mock(PreparedStatement.class);
doReturn(stmt).when(translator).translate(connection, operation);
doReturn("query").when(stmt).toString();
//test
applier.applyMigrationOperation(operation);
//assert
verify(applier, times(1)).applyMigrationOperation(operation);
verify(platform, times(1)).getTranslator();
verify(translator, times(1)).translate(connection, operation);
verify(stmt, times(1)).execute();
verify(stmt, times(1)).close();
verifyNoMoreInteractions(translator, operation, stmt);
verifyNoMoreInteractions(allMocks);
}
@Test
void applyMigrationOperation_translateThrowsSQLException() throws SQLException {
//setup
var translator = mock(MigrationOperationTranslator.class);
doReturn(translator).when(platform).getTranslator();
var operation = mock(MigrationOperation.class);
var stmt = mock(PreparedStatement.class);
var sqlException = new SQLException("reason");
doThrow(sqlException).when(translator).translate(connection, operation);
//test
var thrown = assertThrows(SQLException.class, () -> applier.applyMigrationOperation(operation));
//assert
assertEquals("Failed to build query: reason", thrown.getMessage());
assertSame(sqlException, thrown.getCause());
verify(applier, times(1)).applyMigrationOperation(operation);
verify(platform, times(1)).getTranslator();
verify(translator, times(1)).translate(connection, operation);
verifyNoMoreInteractions(translator, operation, stmt);
verifyNoMoreInteractions(allMocks);
}
@Test
void applyMigrationOperation_stmtExecuteThrowsSQLException() throws SQLException {
//setup
var translator = mock(MigrationOperationTranslator.class);
doReturn(translator).when(platform).getTranslator();
var operation = mock(MigrationOperation.class);
var stmt = mock(PreparedStatement.class);
doReturn(stmt).when(translator).translate(connection, operation);
doReturn("query").when(stmt).toString();
var sqlException = new SQLException("reason");
doThrow(sqlException).when(stmt).execute();
//test
var thrown = assertThrows(SQLException.class, () -> applier.applyMigrationOperation(operation));
//assert
assertEquals("Failed to execute query: query", thrown.getMessage());
assertSame(sqlException, thrown.getCause());
verify(applier, times(1)).applyMigrationOperation(operation);
verify(platform, times(1)).getTranslator();
verify(translator, times(1)).translate(connection, operation);
verify(stmt, times(1)).execute();
verify(stmt, times(1)).close();
verifyNoMoreInteractions(translator, operation, stmt);
verifyNoMoreInteractions(allMocks);
}
@Test
void findMigrations() {
//setup
//test
//assert
}//TODO after function rewrite
}