Статьи

Весна из окопов: использование нулевых значений в наборах данных DbUnit

Если мы пишем интеграционные тесты для приложения, использующего Spring Framework, мы можем интегрировать DbUnit с платформой Spring, используя Spring Test DbUnit .

Тем не менее, эта интеграция не без проблем .

Часто нам нужно вставить нулевые значения в базу данных перед запуском наших тестов или убедиться, что значение, сохраненное в конкретном столбце таблицы, является нулевым . Это очень простые варианты использования, но сложно написать интеграционные тесты, которые их поддерживают.

Этот пост в блоге определяет проблемы, связанные с нулевыми значениями, и описывает, как мы можем их решить. Давайте начнем с краткого обзора тестируемой системы.

Если вы не знаете, как написать интеграционные тесты для своих репозиториев, прочитайте мой пост в блоге под названием: Spring Data JPA Tutorial: Integration Testing .

В нем объясняется, как вы можете написать интеграционные тесты для репозиториев Spring Data JPA, но вы можете использовать тот же подход для написания теста для других репозиториев на основе Spring, которые используют реляционную базу данных.

Тестируемая система

Протестированное «приложение» имеет один объект и один репозиторий Spring Data JPA, который обеспечивает операции CRUD для этого объекта.

Наш класс сущности называется Todo, и соответствующая часть его исходного кода выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import javax.persistence.*;
 
@Entity
@Table(name="todos")
public class Todo {
 
    private static final int MAX_LENGTH_DESCRIPTION = 500;
    private static final int MAX_LENGTH_TITLE = 100;
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(name = "description", nullable = true, length = MAX_LENGTH_DESCRIPTION)
    private String description;
 
    @Column(name = "title", nullable = false, length = MAX_LENGTH_TITLE)
    private String title;
 
    @Version
    private long version;
     
    //Constructors, builder class, and getters are omitted.
}

Кроме того, нам не следует использовать шаблон построителя, поскольку наша сущность имеет только два поля String, которые устанавливаются при создании нового объекта Todo . Однако я использовал его здесь, потому что он облегчает чтение наших тестов.

Наш интерфейс репозитория Spring Data JPA называется TodoRepository , и он расширяет интерфейс CrudRepository <T, ID расширяет Serializable> . Этот репозиторий предоставляет CRUD-операции для объектов Todo . Он также объявляет один метод запроса, который возвращает все записи todo, описание которых соответствует данному поисковому запросу.

Исходный код интерфейса TodoRepository выглядит следующим образом:

1
2
3
4
5
6
import org.springframework.data.repository.CrudRepository;
 
public interface TodoRepository extends CrudRepository<Todo, Long> {
 
    List<Todo> findByDescription(String description);
}

Дополнительное чтение:

Давайте продолжим и выясним, как мы можем обращаться с нулевыми значениями, когда пишем интеграционные тесты для кода, который либо считывает информацию из реляционной базы данных, либо сохраняет информацию в ней.

Работа с нулевыми значениями

Когда мы пишем интеграционные тесты для нашего кода доступа к данным , мы должны инициализировать базу данных в известное состояние перед каждым тестовым примером и гарантировать, что правильные данные записываются в базу данных.

В этом разделе определяются проблемы, с которыми мы сталкиваемся при написании интеграционных тестов,

  • Используйте плоские наборы данных XML.
  • Запишите нулевые значения в базу данных или убедитесь, что значение столбца таблицы равно нулю .

Мы также узнаем, как мы можем решить эти проблемы.

Вставка нулевых значений в базу данных

Когда мы пишем интеграционные тесты, которые читают информацию из базы данных, мы должны инициализировать эту базу данных в известное состояние до того, как наши тесты будут вызваны, и иногда нам приходится вставлять нулевые значения в базу данных.

Поскольку мы используем плоские наборы данных XML, мы можем вставить нулевое значение в столбец таблицы, пропустив соответствующее значение атрибута. Это означает, что если мы хотим вставить нулевое значение в столбец описания таблицы задач , мы можем сделать это, используя следующий набор данных DbUnit:

1
2
3
<dataset>
    <todos id="1" title="FooBar" version="0"/>
</dataset>

Однако часто нам приходится вставлять более одной строки в таблицу используемой базы данных. Следующий набор данных DbUnit ( todo-records.xml ) вставляет две строки в таблицу задач :

1
2
3
4
<dataset>
    <todos id="1" title="FooBar" version="0"/>
    <todos id="2" description="description" title="title" version="0"/>
</dataset>

Давайте выясним, что происходит, когда мы пишем интеграционный тест в метод findByDescription () интерфейса TodoRepository и инициализируем нашу базу данных, используя предыдущий набор данных ( todo-records.xml ). Исходный код нашего интеграционного теста выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
 
import static org.assertj.core.api.Assertions.assertThat;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {PersistenceContext.class})
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
public class ITTodoRepositoryTest {
 
    private static final Long ID = 2L;
    private static final String DESCRIPTION = "description";
    private static final String TITLE = "title";
    private static final long VERSION = 0L;
 
    @Autowired
    private TodoRepository repository;
 
    @Test
    @DatabaseSetup("todo-entries.xml")
    public void findByDescription_ShouldReturnOneTodoEntry() {
        List<Todo> todoEntries = repository.findByDescription(DESCRIPTION);
        assertThat(todoEntries).hasSize(1);
 
        Todo found = todoEntries.get(0);
        assertThat(found.getId()).isEqualTo(ID);
        assertThat(found.getTitle()).isEqualTo(TITLE);
        assertThat(found.getDescription()).isEqualTo(DESCRIPTION);
        assertThat(found.getVersion()).isEqualTo(VERSION);
    }
}

Когда мы запускаем этот интеграционный тест, мы получаем следующую ошибку утверждения:

1
2
java.lang.AssertionError:
Expected size:<1> but was:<0> in: <[]>

Это означает, что правильная запись todo не была найдена в базе данных. Что произошло? Наш метод запроса настолько прост, что он должен был работать, тем более что мы вставили правильные данные в базу данных до того, как наш тестовый пример был вызван.

Ну, на самом деле столбцы описания обеих строк равны нулю. FAQ по DbUnit описывает, почему это произошло :

DbUnit использует первый тег для таблицы, чтобы определить столбцы для заполнения. Если следующие записи для этой таблицы содержат дополнительные столбцы, они не будут заполнены.

Это также обеспечивает решение этой проблемы:

Начиная с DBUnit 2.3.0, существует функция, называемая «распознавание столбцов», которая в основном считывает весь XML в буфер и динамически добавляет новые столбцы по мере их появления.

Мы могли бы решить эту проблему, изменив порядок элементов todos, но это было бы громоздко, потому что мы должны были бы помнить о том, чтобы делать это каждый раз, когда создаем новые наборы данных. Мы должны использовать распознавание столбцов, потому что это исключает возможность человеческой ошибки.

Мы можем включить определение столбца, выполнив следующие действия:

  1. Создайте класс загрузчика набора данных, который расширяет класс AbstractDataSetLoader .
  2. Переопределите защищенный метод IDateSet createDataSet (Resource resource) класса AbstractDataSetLoader .
  3. Реализуйте этот метод, включив распознавание столбцов и возвращая новый объект FlatXmlDataSet .

Исходный код класса ColumnSensingFlatXmlDataSetLoader выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import com.github.springtestdbunit.dataset.AbstractDataSetLoader;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.springframework.core.io.Resource;
import java.io.InputStream;
 
public class ColumnSensingFlatXMLDataSetLoader extends AbstractDataSetLoader {
    @Override
    protected IDataSet createDataSet(Resource resource) throws Exception {
        FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder();
        builder.setColumnSensing(true);
        try (InputStream inputStream = resource.getInputStream()) {
            return builder.build(inputStream);
        }
    }
}

Дополнительное чтение:

Теперь мы можем настроить наш тестовый класс для использования этого загрузчика данных и данных, аннотируя наш тестовый класс аннотацией @DbUnitConfiguration и устанавливая значение его атрибута загрузчика в ColumnSensingFlatXmlDataSetLoader.class .

Исходный код нашего фиксированного интеграционного теста выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
 
import static org.assertj.core.api.Assertions.assertThat;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {PersistenceContext.class})
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DbUnitConfiguration(dataSetLoader = ColumnSensingFlatXMLDataSetLoader.class)
public class ITTodoRepositoryTest {
 
    private static final Long ID = 2L;
    private static final String DESCRIPTION = "description";
    private static final String TITLE = "title";
    private static final long VERSION = 0L;
 
    @Autowired
    private TodoRepository repository;
 
    @Test
    @DatabaseSetup("todo-entries.xml")
    public void findByDescription_ShouldReturnOneTodoEntry() {
        List<Todo> todoEntries = repository.findByDescription(DESCRIPTION);
        assertThat(todoEntries).hasSize(1);
 
        Todo found = todoEntries.get(0);
        assertThat(found.getId()).isEqualTo(ID);
        assertThat(found.getTitle()).isEqualTo(TITLE);
        assertThat(found.getDescription()).isEqualTo(DESCRIPTION);
        assertThat(found.getVersion()).isEqualTo(VERSION);
    }
}

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

Давайте выясним, как мы можем проверить, что нулевые значения сохраняются в базе данных.

Проверка того, что значение столбца таблицы равно нулю

Когда мы пишем интеграционные тесты, которые сохраняют информацию в базе данных, мы должны гарантировать, что правильная информация действительно сохраняется в базе данных, и иногда мы должны проверять, что значение столбца таблицы равно нулю .

Например, если мы пишем интеграционный тест, который проверяет, что правильная информация сохраняется в базе данных, когда мы создаем запись todo, у которой нет описания, мы должны убедиться, что нулевое значение вставлено в столбец описания таблицы todos .

Исходный код нашего интеграционного теста выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
 
import static org.assertj.core.api.Assertions.assertThat;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {PersistenceContext.class})
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DbUnitConfiguration(dataSetLoader = ColumnSensingFlatXMLDataSetLoader.class)
public class ITTodoRepositoryTest {
 
    private static final String DESCRIPTION = "description";
    private static final String TITLE = "title";
 
    @Autowired
    private TodoRepository repository;
 
    @Test
    @DatabaseSetup("no-todo-entries.xml")
    @ExpectedDatabase("save-todo-entry-without-description-expected.xml")
    public void save_WithoutDescription_ShouldSaveTodoEntryToDatabase() {
        Todo todoEntry = Todo.getBuilder()
                .title(TITLE)
                .description(null)
                .build();
 
        repository.save(todoEntry);
    }
}

Это не очень хороший интеграционный тест, потому что он только проверяет, что Spring Data JPA и Hibernate работают правильно. Мы не должны тратить наше время на написание тестов для фреймворков. Если мы не доверяем фреймворку, мы не должны его использовать.

Если вы хотите научиться писать хорошие интеграционные тесты для своего кода доступа к данным, вам следует прочитать мое руководство под названием: « Написание тестов для кода доступа к данным» .

Набор данных DbUnit ( no-todo-records.xml ), который используется для инициализации нашей базы данных, выглядит следующим образом:

1
2
3
<dataset>
    <todos/>
</dataset>

Поскольку мы не задаем описание сохраненной записи задачи, столбец описания таблицы задач должен быть нулевым . Это означает, что мы должны исключить его из набора данных, который проверяет, что правильная информация сохраняется в базе данных.

Этот набор данных ( save-todo-entry-без-description-Ожидаемый.xml ) выглядит следующим образом:

1
2
3
<dataset>
    <todos id="1" title="title" version="0"/>
</dataset>

Когда мы запускаем наш интеграционный тест, он не проходит, и мы видим следующее сообщение об ошибке:

1
2
3
junit.framework.ComparisonFailure: column count (table=todos, expectedColCount=3, actualColCount=4)
Expected :[id, title, version]
Actual   :[DESCRIPTION, ID, TITLE, VERSION]

Проблема в том, что DbUnit ожидает, что таблица задач имеет только столбцы id , title и version . Причина этого заключается в том, что эти столбцы являются единственными столбцами, которые находятся в первой (и единственной) строке нашего набора данных.

Мы можем решить эту проблему с помощью ReplacementDataSet . ReplacementDataSet — это декоратор, который заменяет заполнители, найденные в плоском файле набора XML-данных, объектами замены. Давайте изменим наш пользовательский класс загрузчика набора данных, чтобы он возвращал объект ReplacementDataSet, который заменяет строки ‘[null]’ на null .

Мы можем сделать это, внеся следующие изменения в наш пользовательский загрузчик набора данных:

  1. Добавьте закрытый метод createReplacementDataSet () в класс загрузчика набора данных. Этот метод возвращает объект ReplacementDataSet и принимает объект FlatXmlDataSet в качестве параметра метода.
  2. Реализуйте этот метод, создав новый объект ReplacementDataSet и вернув созданный объект.
  3. Измените метод createDataSet (), чтобы вызвать частный метод createReplacementDataSet () и вернуть созданный объект ReplacementDataSet .

Исходный код класса ColumnSensingReplacementDataSetLoader выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import com.github.springtestdbunit.dataset.AbstractDataSetLoader;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.springframework.core.io.Resource;
 
import java.io.InputStream;
 
public class ColumnSensingReplacementDataSetLoader extends AbstractDataSetLoader {
 
    @Override
    protected IDataSet createDataSet(Resource resource) throws Exception {
        FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder();
        builder.setColumnSensing(true);
        try (InputStream inputStream = resource.getInputStream()) {
            return createReplacementDataSet(builder.build(inputStream));
        }
    }
 
    private ReplacementDataSet createReplacementDataSet(FlatXmlDataSet dataSet) {
        ReplacementDataSet replacementDataSet = new ReplacementDataSet(dataSet);
         
        //Configure the replacement dataset to replace '[null]' strings with null.
        replacementDataSet.addReplacementObject("[null]", null);
         
        return replacementDataSet;
    }
}

Дополнительное чтение:

Мы можем исправить наш интеграционный тест, выполнив следующие действия:

  1. Сконфигурируйте наш тестовый класс для загрузки используемых наборов данных DbUnit с помощью класса ColumnSensingReplacementDataSetLoader .
  2. Измените наш набор данных, чтобы убедиться, что значение столбца описания равно нулю .

Во-первых , нам нужно настроить наш тестовый класс для загрузки наборов данных DbUnit с помощью класса ColumnSensingReplacementDataSetLoader . Поскольку мы уже аннотировали наш тестовый класс с помощью @DbUnitConfiguration , мы должны изменить значение его атрибута загрузчика на ColumnSensingReplacementDataSetLoader.class .

Исходный код фиксированного тестового класса выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
 
import static org.assertj.core.api.Assertions.assertThat;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {PersistenceContext.class})
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class)
public class ITTodoRepositoryTest {
 
    private static final String DESCRIPTION = "description";
    private static final String TITLE = "title";
 
    @Autowired
    private TodoRepository repository;
 
    @Test
    @DatabaseSetup("no-todo-entries.xml")
    @ExpectedDatabase("save-todo-entry-without-description-expected.xml")
    public void save_WithoutDescription_ShouldSaveTodoEntryToDatabase() {
        Todo todoEntry = Todo.getBuilder()
                .title(TITLE)
                .description(null)
                .build();
 
        repository.save(todoEntry);
    }
}

Во-вторых , мы должны убедиться, что нулевое значение сохранено в столбце описания таблицы задач . Мы можем сделать это, добавив атрибут description к единственному элементу todos нашего набора данных и установив значение атрибута description равным ‘[null]’.

Наш фиксированный набор данных ( save-todo-entry-Without-Description-Ожидаемый. XML ) выглядит следующим образом:

1
2
3
<dataset>
    <todos id="1" description="[null]" title="title" version="0"/>
</dataset>

Когда мы запускаем наш интеграционный тест, он проходит.

Давайте продолжим и подведем итог тому, что мы узнали из этого блога.

Резюме

Этот пост научил нас четырем вещам:

  • DbUnit предполагает, что таблица базы данных содержит только те столбцы, которые найдены из первого тега, который определяет столбцы строки таблицы. Если мы хотим переопределить это поведение, мы должны включить функцию распознавания столбцов DbUnit.
  • Если мы хотим убедиться, что нулевое значение сохранено в базе данных, мы должны использовать замещающие наборы данных.
  • Мы узнали, как создать пользовательский загрузчик наборов данных, который создает замещающие наборы данных и использует распознавание столбцов.
  • Мы узнали, как мы можем настроить загрузчик набора данных, который используется для загрузки наших наборов данных DbUnit.

Вы можете получить пример приложения к этому сообщению в блоге от Github .