Создание in-memory базы данных (H2) для unit тестов. Проблема с повторной инициализацией базы в процессе выполнения тестов
Пишу Unit тесты для своего учебного проекта. Перед каждый тестом поднимаю in-memory H2 базу. Часть тестов проходят успешно, а тесты связанные с изменением данных в БД фэйлятся. Ниже код моего коннектора к БД:
public class H2ConnectionSupplier implements Connector{
private final String DB_URL = "jdbc:h2:mem:test;INIT=runscript from '~/AppData/Roaming/JetBrains/IntelliJIdea2020.1/" +
"consoles/db/09fd9eba-5ed4-4312-89fc-e3e185dc7ba6/console.sql'";
private final String DB_USER = "sa";
private final String DB_PASSWORD = "";
public Connection getConnection() throws SQLException {
try {
Connection connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
connection.setAutoCommit(false);
return connection;
} catch (SQLException e) {
e.printStackTrace();
throw new SQLException("No connection with H2 Data Base");
}
}
}
Также привожу код своего репозитория и тестового класса:
public class MusclesGroupRepositoryImp implements Repository<MusclesGroup, Long> {
private final static String INSERT = "INSERT INTO muscles_group (muscles_group_name) VALUES (?)";
private final static String SELECT = "SELECT * FROM muscles_group WHERE id = ?";
private final static String SELECT_ALL = "SELECT * FROM muscles_group";
private final static String DELETE = "DELETE FROM muscles_group WHERE id = ?";
private final static String UPDATE = "UPDATE muscles_group SET muscles_group_name = ?" + " WHERE id = ?";
private Connector connector;
public MusclesGroupRepositoryImp(Connector connector) {
this.connector = connector;
}
@Override
public MusclesGroup findById(Long id) throws TrainingAppRepositoryException {
MusclesGroup musclesGroup = new MusclesGroup();
try(Connection connection = connector.getConnection()) {
try(PreparedStatement preparedStatement = connection.prepareStatement(SELECT, PreparedStatement.RETURN_GENERATED_KEYS)) {
preparedStatement.setLong(1, id);
preparedStatement.executeQuery();
ResultSet resultSet = preparedStatement.getResultSet();
while (resultSet.next()) {
musclesGroup.setId(resultSet.getLong(1));
musclesGroup.setMusclesGroupName(resultSet.getString(2));
}
connection.commit();
} catch (SQLException e) {
connection.rollback();
throw new TrainingAppRepositoryException("Error while trying to find exercise id = " + id);
}
} catch (SQLException e) {
throw new TrainingAppRepositoryException("Error while trying to find exercise id = " + id);
}
return musclesGroup;
}
@Override
public List<MusclesGroup> findAll() throws SQLException {
List<MusclesGroup>list = new ArrayList<>();
try(Connection connection = connector.getConnection()) {
try(PreparedStatement preparedStatement = connection.prepareStatement(SELECT_ALL, PreparedStatement.RETURN_GENERATED_KEYS)) {
preparedStatement.executeQuery();
ResultSet resultSet = preparedStatement.getResultSet();
while (resultSet.next()) {
MusclesGroup musclesGroup = new MusclesGroup();
musclesGroup.setId(resultSet.getLong(1));
musclesGroup.setMusclesGroupName(resultSet.getString(2));
list.add(musclesGroup);
}
connection.commit();
} catch (SQLException e) {
connection.rollback();
throw new TrainingAppRepositoryException("Error while trying to find all " + MusclesGroup.class.getSimpleName());
}
} catch (SQLException e) {
throw new TrainingAppRepositoryException("Error while trying to find all " + MusclesGroup.class.getSimpleName());
}
return list;
}
@Override
public void remove(Long id) throws SQLException {
try(Connection connection = connector.getConnection()) {
try(PreparedStatement preparedStatement = connection.prepareStatement(DELETE)) {
preparedStatement.setLong(1, id);
preparedStatement.executeUpdate();
connection.commit();
} catch (SQLException e) {
connection.rollback();
throw new TrainingAppRepositoryException("Error while removing muscle group id = " + id);
}
} catch (SQLException e) {
throw new TrainingAppRepositoryException("Error while removing muscle group id = " + id);
}
}
@Override
public MusclesGroup save(MusclesGroup musclesGroup) throws SQLException {
try(Connection connection = connector.getConnection()) {
try(PreparedStatement preparedStatement = connection.prepareStatement(INSERT, Statement.RETURN_GENERATED_KEYS)) {
preparedStatement.setString(1, musclesGroup.getMusclesGroupName());
preparedStatement.executeUpdate();
connection.commit();
} catch (SQLException e) {
connection.rollback();
throw new TrainingAppRepositoryException("Error while saving muscle group - " + musclesGroup.getMusclesGroupName());
}
} catch (SQLException e) {
throw new TrainingAppRepositoryException("Error while saving muscle group - " + musclesGroup.getMusclesGroupName());
}
return musclesGroup;
}
@Override
public void update(MusclesGroup model, Long id) throws SQLException {
try(Connection connection = connector.getConnection()) {
try(PreparedStatement preparedStatement = connection.prepareStatement(UPDATE)) {
preparedStatement.setLong(2, id);
preparedStatement.setString(1, model.getMusclesGroupName());
preparedStatement.executeUpdate();
connection.commit();
} catch (SQLException e) {
connection.rollback();
e.getCause();
e.getMessage();
}
} catch (SQLException e) {
e.printStackTrace();
e.getCause();
}
}
}
Тесты:
public class MuscleGroupRepositoryImplTest {
private Connector connector = new H2ConnectionSupplier();
private MusclesGroupRepositoryImp musclesGroupRepositoryImp = new MusclesGroupRepositoryImp(connector);
@SneakyThrows
@ParameterizedTest
@ValueSource(longs = {1})
@DisplayName("findById method test")
void findByIdTest(Long id) {
MusclesGroup musclesGroup = musclesGroupRepositoryImp.findById(id);
assertNotNull(musclesGroup);
assertAll("Проверка полей объекта",
() -> assertEquals(1L, musclesGroup.getId()),
() -> assertEquals("Грудь", musclesGroup.getMusclesGroupName())
);
}
@DisplayName("save method test")
@Test
void saveTest() throws SQLException {
MusclesGroup musclesGroup = new MusclesGroup();
musclesGroup.setMusclesGroupName("Икры");
assertNotNull(musclesGroupRepositoryImp.save(musclesGroup));
}
@SneakyThrows
@ParameterizedTest
@ValueSource(longs = {1})
@DisplayName("update method test")
void updateTest(Long id) {
MusclesGroup musclesGroup1 = new MusclesGroup(1L, "Грудь");
MusclesGroup musclesGroup = new MusclesGroup();
musclesGroup.setMusclesGroupName("Руки");
musclesGroupRepositoryImp.update(musclesGroup, id);
String actual = musclesGroupRepositoryImp.findById(id).getMusclesGroupName();
assertNotNull(musclesGroup);
assertNotEquals(musclesGroup1.getMusclesGroupName(), actual);
}
@SneakyThrows
@ParameterizedTest
@ValueSource(longs = {2})
@DisplayName("remove method test")
void removeTest(Long id) {
musclesGroupRepositoryImp.remove(id);
assertNull(musclesGroupRepositoryImp.findById(id));
}
@DisplayName("findAll method test")
@Test
void findAllTest() throws SQLException {
List<MusclesGroup> musclesGroupList = musclesGroupRepositoryImp.findAll();
assertNotNull(musclesGroupList);
assertAll("Проверка полученных объектов из БД",
() -> assertEquals(new MusclesGroup(1L, "Грудь").toString(), musclesGroupList.get(0).toString()),
() -> assertEquals(new MusclesGroup(2L, "Спина").toString(), musclesGroupList.get(1).toString()),
() -> assertEquals(new MusclesGroup(3L, "Ноги").toString(), musclesGroupList.get(2).toString()),
() -> assertEquals(new MusclesGroup(4L, "Плечи").toString(), musclesGroupList.get(3).toString()),
() -> assertEquals(new MusclesGroup(5L, "Бицепс").toString(), musclesGroupList.get(4).toString()),
() -> assertEquals(new MusclesGroup(6L, "Пресс").toString(), musclesGroupList.get(5).toString())
);
}
}
В тестах update и remove, при повторном обращении к базе (например, вызов метода musclesGroupRepositoryImp.findById(id), чтобы убедиться в том, что данные изменены / удалены), возвращаются исходные значение записей БД, а не обновленные после вызова соответствующих методов.
Я понимаю, что проблема заключается как раз в объекте musclesGroupRepositoryImp и именно повторный вызов метода через этот инстанс поднимает новую БД. Но не могу пока придумать как по-другому организовать создание in memory БД. В документации по H2 и иных ресурсах найти решение не удалось.
На всякий случай также привожу свой SQL скрипт:
create schema training_app;
create table muscles_group (
id INT primary key auto_increment not null,
muscles_group_name VARCHAR(500) not null,
create_date TIMESTAMP default CURRENT_TIMESTAMP not null,
update_date TIMESTAMP default now() on update now() not null
);
create table muscles_subgroup (
id INT primary key auto_increment not null,
muscles_group_id INT not null,
muscles_subgroup_name VARCHAR(500) not null ,
create_date TIMESTAMP default CURRENT_TIMESTAMP not null ,
update_date TIMESTAMP default now() on update now() not null ,
foreign key (muscles_group_id) references muscles_group(id)
);
insert into muscles_group (muscles_group_name) values
('Грудь'),
('Спина'),
('Ноги'),
('Плечи'),
('Бицепс'),
('Пресс');
Ответы (2 шт):
для инициализации БД в тестах ты можешь использовать аннотацию @Before либо @BeforeAll.
@Before - Создаст единую копию локальной БД перед всеми тестами.
@BeforeAll - Будет создавать новую копию БД перед каждым запуском нового теста.
Пример:
@Before
public void setUp() {
connector = new H2ConnectionSupplier();
musclesGroupRepositoryImp = new MusclesGroupRepositoryImp(connector);
}
Либо:
@BeforeAll
public void setUp() {
connector = new H2ConnectionSupplier();
musclesGroupRepositoryImp = new MusclesGroupRepositoryImp(connector);
}
Проблема действительно возникает из-за того, что используется БД в памяти в режиме, когда закрытие последнего соединения уничтожает БД.
Вот что говорит документация:
By default, closing the last connection to a database closes the database. For an in-memory database, this means the content is lost.
Исправить это можно используя дополнительный параметр в jdbc URL, описанный там же, а именно DB_CLOSE_DELAY=-1. Значение -1 говорит, что БД не будет уничтожаться пока не остановится JVM.
Это правда приведет к другой проблеме, а именно к тому, как вы используете параметр INIT в jdbc URL. Он указывает скрипт, который выполняется при установлении соединения к БД. Сейчас каждое соединение работает со свой копией базы созданной с нуля. После того, как будет добавлен DB_CLOSE_DELAY и работа с БД будет вестись, так как это обычно происходит в реальности (т.е. закрытие соединения не будет убивать БД), то при повторном открытии соединения возникнут ошибки, что объекты которые пробует создать INIT скрипт уже существуют.
Проблема в том, что параметр INIT не годится для этой задачи. Нужно, чтобы инициализация делалась один раз на тест, а не при каждом соединении. То есть нужно, чтобы каждый тест перед запуском инициализировал БД и приводил ее к изначальному состоянию.
Вариантов как это сделать много.
Один вариант, это перед каждым тестом (в @BeforeEach) выполнять скрипт, который почистит БД:
RunScript.execute(conn, new FileReader("~/AppData/Roaming/JetBrains/IntelliJIdea2020.1/" +
"consoles/db/09fd9eba-5ed4-4312-89fc-e3e185dc7ba6/console.sql"));
Чтобы он нормально работал при повторных запусках, нужно вначале добавить:
DROP ALL OBJECTS;