1. Введение
В этом руководстве мы рассмотрим DBUnit, инструмент модульного тестирования, используемый для проверки взаимодействия реляционных баз данных в Java.
Мы увидим, как это поможет нам привести нашу базу данных в известное состояние и подтвердить ожидаемое состояние.
2. Зависимости
Во-первых, мы можем добавить DBUnit в наш проект из Maven Central, добавив зависимость dbunit
к нашему pom.xml
:
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.7.0</version>
<scope>test</scope>
</dependency>
Мы можем поискать самую последнюю версию на Maven Central .
3. Пример «Привет, мир»
Далее давайте определим схему базы данных:
схема.sql
:
CREATE TABLE IF NOT EXISTS CLIENTS
(
`id` int AUTO_INCREMENT NOT NULL,
`first_name` varchar(100) NOT NULL,
`last_name` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE IF NOT EXISTS ITEMS
(
`id` int AUTO_INCREMENT NOT NULL,
`title` varchar(100) NOT NULL,
`produced` date,
`price` float,
PRIMARY KEY (`id`)
);
3.1. Определение начального содержимого базы данных
DBUnit позволяет нам определять и загружать наш тестовый набор данных простым декларативным способом .
Мы определяем каждую строку таблицы с одним элементом XML, где имя тега является именем таблицы, а имена и значения атрибутов сопоставляются с именами и значениями столбцов соответственно. Данные строки могут быть созданы для нескольких таблиц. Нам нужно реализовать метод getDataSet ()
DataSourceBasedDBTestCase
, чтобы определить начальный набор данных, где мы можем использовать FlatXmlDataSetBuilder
для ссылки на наш файл XML:
данные.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<CLIENTS id='1' first_name='Charles' last_name='Xavier'/>
<ITEMS id='1' title='Grey T-Shirt' price='17.99' produced='2019-03-20'/>
<ITEMS id='2' title='Fitted Hat' price='29.99' produced='2019-03-21'/>
<ITEMS id='3' title='Backpack' price='54.99' produced='2019-03-22'/>
<ITEMS id='4' title='Earrings' price='14.99' produced='2019-03-23'/>
<ITEMS id='5' title='Socks' price='9.99'/>
</dataset>
3.2. Инициализация подключения к базе данных и схемы
Теперь, когда у нас есть схема, мы должны инициализировать нашу базу данных.
Нам нужно расширить класс DataSourceBasedDBTestCase
и инициализировать схему базы данных в его методе getDataSource()
:
DataSourceDBUnitTest.java
:
public class DataSourceDBUnitTest extends DataSourceBasedDBTestCase {
@Override
protected DataSource getDataSource() {
JdbcDataSource dataSource = new JdbcDataSource();
dataSource.setURL(
"jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;init=runscript from 'classpath:schema.sql'");
dataSource.setUser("sa");
dataSource.setPassword("sa");
return dataSource;
}
@Override
protected IDataSet getDataSet() throws Exception {
return new FlatXmlDataSetBuilder().build(getClass().getClassLoader()
.getResourceAsStream("data.xml"));
}
}
Здесь мы передали файл SQL в базу данных H2 в памяти в строке подключения. Если мы хотим протестировать другие базы данных, нам нужно будет предоставить для этого собственную реализацию.
Имейте в виду, что в нашем примере DBUnit будет повторно инициализировать базу данных с заданными тестовыми данными перед каждым выполнением тестового метода .
Есть несколько способов настроить это через get
SetUpOperation
и TearDownOperation
:
@Override
protected DatabaseOperation getSetUpOperation() {
return DatabaseOperation.REFRESH;
}
@Override
protected DatabaseOperation getTearDownOperation() {
return DatabaseOperation.DELETE_ALL;
}
Операция REFRESH
указывает DBUnit обновить все свои данные. Это гарантирует, что все кеши будут очищены, и наш модульный тест не будет зависеть от другого модульного теста. Операция DELETE_ALL
обеспечивает удаление всех данных в конце каждого модульного теста. В нашем случае мы сообщаем DBUnit, что во время установки, используя реализацию метода getSetUpOperation
, мы обновим все кеши. Наконец, мы говорим DBUnit удалить все данные во время операции разрыва, используя реализацию метода getTearDownOperation
.
3.3. Сравнение ожидаемого состояния и фактического состояния
Теперь давайте рассмотрим наш фактический тестовый пример. Для этого первого теста мы сохраним простоту — мы загрузим ожидаемый набор данных и сравним его с набором данных, полученным из нашего соединения с БД:
@Test
public void givenDataSetEmptySchema_whenDataSetCreated_thenTablesAreEqual() throws Exception {
IDataSet expectedDataSet = getDataSet();
ITable expectedTable = expectedDataSet.getTable("CLIENTS");
IDataSet databaseDataSet = getConnection().createDataSet();
ITable actualTable = databaseDataSet.getTable("CLIENTS");
assertEquals(expectedTable, actualTable);
}
4. Глубокое погружение в утверждения
В предыдущем разделе мы видели базовый пример сравнения фактического содержимого таблицы с ожидаемым набором данных. Теперь мы собираемся открыть поддержку DBUnit для настройки утверждений данных.
4.1. Утверждение с помощью SQL-запроса
Простой способ проверить фактическое состояние — это запрос SQL .
В этом примере мы вставим новую запись в таблицу CLIENTS, а затем проверим содержимое только что созданной строки. Мы определили ожидаемый результат в отдельном файле XML и извлекли фактическое значение строки с помощью SQL-запроса:
@Test
public void givenDataSet_whenInsert_thenTableHasNewClient() throws Exception {
try (InputStream is = getClass().getClassLoader().getResourceAsStream("dbunit/expected-user.xml")) {
IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(is);
ITable expectedTable = expectedDataSet.getTable("CLIENTS");
Connection conn = getDataSource().getConnection();
conn.createStatement()
.executeUpdate(
"INSERT INTO CLIENTS (first_name, last_name) VALUES ('John', 'Jansen')");
ITable actualData = getConnection()
.createQueryTable(
"result_name",
"SELECT * FROM CLIENTS WHERE last_name='Jansen'");
assertEqualsIgnoreCols(expectedTable, actualData, new String[] { "id" });
}
}
Метод getConnection()
класса- предка DBTestCase
возвращает специфичное для DBUnit представление соединения с источником данных ( экземпляр IDatabaseConnection
). Метод createQueryTable()
IDatabaseConnection можно
использовать для извлечения фактических данных из базы данных для сравнения с ожидаемым состоянием базы данных с помощью метода Assertion.assertEquals()
. SQL-запрос, переданный в функцию createQueryTable()
, — это запрос, который мы хотим протестировать. Он возвращает экземпляр таблицы
, который мы используем для утверждения.
4.2. Игнорирование столбцов
Иногда в тестах базы данных мы хотим игнорировать некоторые столбцы реальных таблиц . Обычно это автоматически сгенерированные значения, которые мы не можем строго контролировать, например сгенерированные первичные ключи или текущие метки времени .
Мы могли бы сделать это, исключив столбцы из предложений SELECT в SQL-запросах, но DBUnit предоставляет для этого более удобную утилиту. С помощью статических методов класса DefaultColumnFilter
мы можем создать новый экземпляр ITable
из существующего, исключив некоторые столбцы , как показано здесь:
@Test
public void givenDataSet_whenInsert_thenGetResultsAreStillEqualIfIgnoringColumnsWithDifferentProduced()
throws Exception {
Connection connection = tester.getConnection().getConnection();
String[] excludedColumns = { "id", "produced" };
try (InputStream is = getClass().getClassLoader()
.getResourceAsStream("dbunit/expected-ignoring-registered_at.xml")) {
IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(is);
ITable expectedTable = excludedColumnsTable(expectedDataSet.getTable("ITEMS"), excludedColumns);
connection.createStatement()
.executeUpdate("INSERT INTO ITEMS (title, price, produced) VALUES('Necklace', 199.99, now())");
IDataSet databaseDataSet = tester.getConnection().createDataSet();
ITable actualTable = excludedColumnsTable(databaseDataSet.getTable("ITEMS"), excludedColumns);
assertEquals(expectedTable, actualTable);
}
}
4.3. Расследование множественных отказов
Если DBUnit находит неверное значение, он немедленно выдает AssertionError
.
В определенных случаях мы можем использовать класс DiffCollectingFailureHandler
, который мы можем передать методу Assertion.assertEquals()
в качестве третьего аргумента.
Этот обработчик сбоев будет собирать все сбои вместо того, чтобы останавливаться на первом, а это означает, что метод Assertion.assertEquals()
всегда будет успешным, если мы используем DiffCollectingFailureHandler .
Поэтому нам придется программно проверять, не нашел ли обработчик ошибок:
@Test
public void givenDataSet_whenInsertUnexpectedData_thenFailOnAllUnexpectedValues() throws Exception {
try (InputStream is = getClass().getClassLoader()
.getResourceAsStream("dbunit/expected-multiple-failures.xml")) {
IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(is);
ITable expectedTable = expectedDataSet.getTable("ITEMS");
Connection conn = getDataSource().getConnection();
DiffCollectingFailureHandler collectingHandler = new DiffCollectingFailureHandler();
conn.createStatement()
.executeUpdate("INSERT INTO ITEMS (title, price) VALUES ('Battery', '1000000')");
ITable actualData = getConnection().createDataSet().getTable("ITEMS");
assertEquals(expectedTable, actualData, collectingHandler);
if (!collectingHandler.getDiffList().isEmpty()) {
String message = (String) collectingHandler.getDiffList()
.stream()
.map(d -> formatDifference((Difference) d))
.collect(joining("\n"));
logger.error(() -> message);
}
}
}
private static String formatDifference(Difference diff) {
return "expected value in " + diff.getExpectedTable()
.getTableMetaData()
.getTableName() + "." +
diff.getColumnName() + " row " +
diff.getRowIndex() + ":" +
diff.getExpectedValue() + ", but was: " +
diff.getActualValue();
}
Кроме того, обработчик предоставляет сбои в виде экземпляров Difference
, что позволяет нам форматировать ошибки.
После запуска теста получаем отформатированный отчет:
java.lang.AssertionError: expected value in ITEMS.price row 5:199.99, but was: 1000000.0
expected value in ITEMS.produced row 5:2019-03-23, but was: null
expected value in ITEMS.title row 5:Necklace, but was: Battery
at com.foreach.dbunit.DataSourceDBUnitTest.givenDataSet_whenInsertUnexpectedData_thenFailOnAllUnexpectedValues(DataSourceDBUnitTest.java:91)
Важно отметить, что в этот момент мы ожидали, что новый предмет будет иметь цену 199,99, но это было 1000000,0. Затем мы видим, что дата производства должна быть 2019-03-23, но в итоге она оказалась нулевой. Наконец, ожидаемым предметом было Ожерелье, а вместо него мы получили Батарейку.
5. Вывод
В этой статье мы увидели, как DBUnit предоставляет декларативный способ определения тестовых данных для тестирования уровней доступа к данным приложений Java.
Как всегда, полный исходный код примеров доступен на GitHub .