«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. Пример Hello World

Далее определим схему базы данных:

schema.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:

data.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 будет повторно инициализировать базу данных с заданными тестовыми данными перед выполнением каждого тестового метода.

Есть несколько способов настроить это с помощью getSetUpOperation и getTearDownOperation:

@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.baeldung.dbunit.DataSourceDBUnitTest.givenDataSet_whenInsertUnexpectedData_thenFailOnAllUnexpectedValues(DataSourceDBUnitTest.java:91)

Важно отметить, что в этот момент мы ожидали, что новый предмет будет иметь цену 199,99, но это было 1000000,0. Затем мы видим, что дата производства должна быть 2019-03-23, но в итоге она оказалась нулевой. Наконец, ожидаемым предметом было Ожерелье, а вместо него мы получили Батарейку.

5. Заключение

В этой статье мы увидели, как DBUnit предоставляет декларативный способ определения тестовых данных для тестирования слоев доступа к данным Java-приложений.

Как всегда, полный исходный код примеров доступен на GitHub.