Перейти к основному содержимому

Введение в DBUnit

· 7 мин. чтения

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 .