如果我们正在为使用Spring Framework的应用程序编写集成测试,则可以通过使用Spring Test DbUnit将DbUnit与Spring测试框架集成。
但是, 这种集成并非没有问题 。
通常,我们必须在运行测试之前向数据库中插入空值,或者验证保存到特定表列中的值是否为空 。 这些是非常基本的用例,但是编写支持它们的集成测试非常棘手。
这篇博客文章指出了与null值有关的问题,并描述了如何解决它们。 让我们从快速查看被测系统开始。
如果您不知道如何为存储库编写集成测试,则应阅读我的博客文章,标题为: Spring Data JPA教程:集成测试 。
它解释了如何为Spring Data JPA存储库编写集成测试,但是可以使用相同的方法为其他使用关系数据库的Spring支持的存储库编写测试。
被测系统
经过测试的“应用程序”具有一个实体和一个Spring Data JPA存储库,该存储库为该实体提供CRUD操作。
我们的实体类称为Todo ,其源代码的相关部分如下所示:
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.
}
- 您可以从Github获取Todo类的完整源代码 。
另外,我们不应该使用构建器模式,因为在创建新的Todo对象时,我们的实体只有两个String字段被设置。 但是,我在这里使用它是因为它使我们的测试更易于阅读。
我们的Spring Data JPA存储库接口称为TodoRepository ,它扩展了CrudRepository <T,ID扩展了Serializable>接口。 该存储库为Todo对象提供CRUD操作。 它还声明一种查询方法,该方法返回其说明与给定搜索词匹配的所有待办事项条目。
TodoRepository接口的源代码如下所示:
import org.springframework.data.repository.CrudRepository;
public interface TodoRepository extends CrudRepository<Todo, Long> {
List<Todo> findByDescription(String description);
}
补充阅读:
让我们继续前进,了解在编写用于从关系数据库读取信息或将信息保存到其中的代码的集成测试时,如何处理空值。
处理空值
在为数据访问代码编写集成测试时 ,我们必须在每个测试用例之前将数据库初始化为已知状态,并确保将正确的数据写入数据库。
本节确定了在编写集成测试以解决我们遇到的问题
- 使用平面XML数据集。
- 将空值写入数据库或确保表列的值为null 。
我们还将学习如何解决这些问题。
将空值插入数据库
当我们编写从数据库读取信息的集成测试时,必须在调用测试之前将该数据库初始化为已知状态,有时我们必须向数据库中插入空值。
因为我们使用平面XML数据集,所以可以通过省略相应的属性值将空值插入到表列中。 这意味着,如果我们想在todos表的description列中插入null值,则可以通过使用以下DbUnit数据集来做到这一点:
<dataset>
<todos id="1" title="FooBar" version="0"/>
</dataset>
但是,通常我们必须在已使用的数据库表中插入多行。 以下DbUnit数据集( todo-entries.xml )将两行插入todos表:
<dataset>
<todos id="1" title="FooBar" version="0"/>
<todos id="2" description="description" title="title" version="0"/>
</dataset>
让我们找出对TodoRepository接口的findByDescription()方法进行集成测试并使用先前的数据集( todo-entries.xml )初始化数据库时发生的情况。 我们的集成测试的源代码如下所示:
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);
}
}
当运行此集成测试时,会出现以下断言错误:
java.lang.AssertionError:
Expected size:<1> but was:<0> in: <[]>
这意味着从数据库中找不到正确的待办事项条目。 发生了什么? 我们的查询方法是如此简单,以至于它应该起作用,特别是因为在调用测试用例之前,我们已将正确的数据插入数据库。
好吧,实际上两行的描述列都是空的。 DbUnit常见问题说明了发生这种情况的原因 :
DbUnit使用表的第一个标记来定义要填充的列。 如果此表的以下记录包含额外的列,那么将不会填充这些列。
它还提供了解决此问题的方法:
从DBUnit 2.3.0开始,有一种称为“列检测”的功能,该功能基本上将整个XML读入缓冲区,并在出现新列时动态添加它们。
我们可以通过反转todos元素的顺序来解决此问题,但这很麻烦,因为每次创建新数据集时我们都必须记住要做的事情。 我们应该使用列检测,因为它消除了人为错误的可能性。
我们可以按照以下步骤启用列检测:
- 创建一个扩展AbstractDataSetLoader类的数据集加载器类。
- 重写AbstractDataSetLoader类的受保护的IDateSet createDataSet(Resource resource)方法。
- 通过启用列感应并返回新的FlatXmlDataSet对象来实现此方法。
ColumnSensingFlatXmlDataSetLoader类的源代码如下所示:
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,来配置测试类以使用此数据加载器 。
我们的固定集成测试的源代码如下所示:
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);
}
}
当我们第二次运行集成测试时,它通过了。
让我们找出如何验证空值是否已保存到数据库中。
验证表列的值是否为空
在编写将信息保存到数据库的集成测试时,我们必须确保将正确的信息确实保存到数据库中,有时我们必须验证表列的值为null 。
例如,如果我们写这证实了,当我们创建一个没有描述一个待办事项条目正确的信息保存到数据库中的集成测试,我们必须确保一个空值插入到待办事项表的说明列。
我们的集成测试的源代码如下所示:
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-entries.xml )如下所示:
<dataset>
<todos/>
</dataset>
因为我们没有设置保存的todo条目的描述 ,所以todos表的description列应该为null 。 这意味着我们应该从数据集中省略它,以验证是否将正确的信息保存到数据库中。
该数据集( save-todo-entry-without-description-expected.xml )如下所示:
<dataset>
<todos id="1" title="title" version="0"/>
</dataset>
当我们运行集成测试时,它失败,并且我们看到以下错误消息:
junit.framework.ComparisonFailure: column count (table=todos, expectedColCount=3, actualColCount=4)
Expected :[id, title, version]
Actual :[DESCRIPTION, ID, TITLE, VERSION]
问题在于DbUnit期望todos表仅具有id , title和version列。 这样做的原因是,这些列是从数据集的第一行(也是唯一的行)中找到的唯一列。
我们可以使用ReplacementDataSet解决此问题。 ReplacementDataSet是一个装饰器,它用替换对象替换从平面XML数据集文件中找到的占位符。 让我们修改自定义的数据集加载类返回一个ReplacementDataSet对象替换“[空]”字符串与空 。
我们可以通过对自定义数据集加载器进行以下更改来做到这一点:
- 将私有的createReplacementDataSet()方法添加到数据集加载器类。 此方法返回一个ReplacementDataSet对象,并将FlatXmlDataSet对象作为方法参数。
- 通过创建新的ReplacementDataSet对象并返回创建的对象来实现此方法。
- 修改createDataSet()方法以调用私有的createReplacementDataSet()方法并返回创建的ReplacementDataSet对象。
ColumnSensingReplacementDataSetLoader类的源代码如下所示:
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;
}
}
补充阅读:
我们可以按照以下步骤修复集成测试:
- 通过使用ColumnSensingReplacementDataSetLoader类,配置我们的测试类以加载使用的DbUnit数据集。
- 修改我们的数据集以验证description列的值为null 。
首先 ,我们必须配置测试类以使用ColumnSensingReplacementDataSetLoader类加载DbUnit数据集。 因为我们已经使用@DbUnitConfiguration为测试类添加了注释 ,所以我们必须将其loader属性的值更改为ColumnSensingReplacementDataSetLoader.class 。
固定测试类的源代码如下所示:
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);
}
}
其次 ,我们必须验证是否将空值保存到todos表的description列中。 为此,我们可以向数据集中唯一的todos元素添加一个description属性,并将description属性的值设置为'[null]'。
我们的固定数据集( save-todo-entry-without-description-expected.xml )如下所示:
<dataset>
<todos id="1" description="[null]" title="title" version="0"/>
</dataset>
当我们运行集成测试时,它会通过。
让我们继续并总结从这篇博客文章中学到的知识。
摘要
这篇博客文章教会了我们四件事:
- DbUnit假定数据库表仅包含从指定表行的列的第一个标记中找到的那些列。 如果要覆盖此行为,则必须启用DbUnit的列感测功能。
- 如果要确保将null值保存到数据库,则必须使用替换数据集。
- 我们了解了如何创建自定义数据集加载器,该加载器创建替换数据集并使用列感测。
- 我们了解了如何配置用于加载DbUnit数据集的数据集加载器。