摆脱困境:在每种测试方法之前重置自动增量列

当我们为将信息保存到数据库的功能编写集成测试时,我们必须验证是否将正确的信息保存到数据库。

如果我们的应用程序使用Spring Framework,则可以为此目的使用Spring Test DbUnitDbUnit

但是,很难验证是否在主键列中插入了正确的值,因为主键通常是通过使用自动增量或序列自动生成的。

这篇博客文章指出了与自动生成值的列相关的问题,并帮助我们解决了这一问题。

补充阅读:
  • 在标题为“ 从沟壑中弹跳:在DbUnit数据集中使用空值 ”的博客文章中描述了经过测试的应用程序。 我建议您阅读该博客文章,因为我不会在此博客文章上重复其内容。
  • 如果您不知道如何为存储库编写集成测试,则应阅读我的博客文章,标题为: Spring Data JPA教程:集成测试 。 它说明了如何为Spring Data JPA存储库编写集成测试,但是您可以使用与为使用关系数据库的其他Spring Powered存储库编写测试的方法相同的方法。

我们无法断言未知

让我们开始为CrudRepository接口的save()方法编写两个集成测试。 这些测试描述如下:

  • 第一个测试确保在设置了保存的Todo对象的标题和描述时将正确的信息保存到数据库中。
  • 当仅设置了保存的Todo对象的标题时,第二个测试将验证是否将正确的信息保存到数据库中。

这两个测试都通过使用相同的DbUnit数据集( no-todo-entries.xml )初始化使用的数据库,该数据集如下所示:

<dataset>
    <todos/>
</dataset>

我们的集成测试类的源代码如下所示:

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.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
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;

@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 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("no-todo-entries.xml")
    @ExpectedDatabase("save-todo-entry-with-title-and-description-expected.xml")
    public void save_WithTitleAndDescription_ShouldSaveTodoEntryToDatabase() {
        Todo todoEntry = Todo.getBuilder()
                .title(TITLE)
                .description(DESCRIPTION)
                .build();

        repository.save(todoEntry);
    }

    @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数据集( save-todo-entry-with-title-and-description-expected.xml )用于验证已保存的Todo对象的标题和描述是否已插入todos表中,如下所示:

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

DbUnit数据集( save-todo-entry-without-description-expected.xml )用于验证是否仅将已保存的Todo对象的标题插入了todos表,如下所示:

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

当我们运行集成测试时,其中之一失败,并且我们看到以下错误消息:

junit.framework.ComparisonFailure: value (table=todos, row=0, col=id) 
Expected :1
Actual   :2

原因是todos表的id列是一个自动增量列,并且首先调用的集成测试“获取”了id1。在调用第二个集成测试时,值2保存到了id列,测试失败。

让我们找出如何解决这个问题。

快速修复赢?

有两个快速解决我们的问题的方法。 这些修复程序描述如下:

首先 ,我们可以使用@DirtiesContext批注注释测试类,并将其classMode属性的值设置为DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD。这将解决我们的问题,因为在加载应用程序上下文时,我们的应用程序会创建一个新的内存数据库,并且@DirtiesContext注释可确保每个测试方法都使用新的应用程序上下文。

我们的测试类的配置如下所示:

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.annotation.DirtiesContext;
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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {PersistenceContext.class})
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DbUnitConfiguration(dataSetLoader = ColumnSensingReplacementDataSetLoader.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class ITTodoRepositoryTest {

}

这看起来很干净,但不幸的是,它可能会破坏我们的集成测试套件的性能,因为它会在调用每个测试方法之前创建一个新的应用程序上下文。 这就是为什么我们不应该使用@DirtiesContext批注,除非它是绝对必要的

但是,如果我们的应用程序只有少量的集成测试,则@DirtiesContext批注引起的性能损失可能是可以容忍的。 我们不应该仅仅因为它会使我们的测试变慢而放弃该解决方案。 有时这是可以接受的,并且在这种情况下,使用@DirtiesContext注释是一个很好的解决方案。

补充阅读:

其次 ,我们可以从数据集中省略todos元素的id属性,并将@ExpectedDatabase批注的assertionMode属性的值设置为DatabaseAssertionMode.NON_STRICT 。 这将解决我们的问题,因为DatabaseAssertionMode.NON_STRICT意味着将忽略数据集文件中不存在的列和表。

该断言模式是一个有用的工具,因为它使我们有可能忽略其信息不会被测试代码更改的表。 但是, DatabaseAssertionMode.NON_STRICT不是解决此特定问题的正确工具,因为它迫使我们编写用于验证很少内容的数据集。

例如,我们不能使用以下数据集:

<dataset>
	<todos id="1" description="description" title="title" version="0"/>
	<todos description="description two" title="title two" version="0"/>
</dataset>

如果我们使用DatabaseAssertionMode.NON_STRICT ,则数据集的每个“行”必须指定相同的列。 换句话说,我们必须将数据集修改为如下所示:

<dataset>
	<todos description="description" title="title" version="0"/>
	<todos description="description two" title="title two" version="0"/>
</dataset>

没什么大不了的,因为我们可以相信Hibernate将正确的ID插入todos表的id列中。

但是,如果每个待办事项条目都可以包含0 .. *标签,那么我们将会遇到麻烦。 假设我们必须编写一个集成测试,该测试将两个新的todo条目插入数据库并创建一个DbUnit数据集,以确保

  • 标题为“ title one”的待办事项条目具有一个名为“ tag one”的标签。
  • 标题为“标题二”的待办事项条目具有名为“标签二”的标签。

我们的最大努力如下所示:

<dataset>
	<todos description="description" title="title one" version="0"/>
	<todos description="description two" title="title two" version="0"/>
	
	<tags name="tag one" version="0"/>
	<tags name="tag two" version="0"/>
</dataset>

我们无法创建有用的DbUnit数据集,因为我们不知道保存到数据库中的待办事项条目的ID。

我们必须找到更好的解决方案。

寻找更好的解决方案

我们已经为我们的问题找到了两种不同的解决方案,但是它们都产生了新的问题。 第三种解决方案基于以下思想:

如果我们不知道插入到自动增量列中的下一个值,则必须在调用每个测试方法之前重置自动增量列。

我们可以按照以下步骤进行操作:

  1. 创建一个用于重置指定数据库表的自动增量列的类。
  2. 修复我们的集成测试。

让我们弄脏双手。

创建可以重置自动增量列的类

我们可以通过执行以下步骤来创建该类,该类可以重置指定数据库表的自动增量列:

  1. 创建一个名为DbTestUtil最终类,并通过向其添加私有构造函数来防止其实例化。
  2. 公共静态void resetAutoIncrementColumns()方法添加到DbTestUtil类。 此方法采用两个方法参数:
    1. ApplicationContext对象包含测试的应用程序的配置。
    2. 必须重置其自动增量列的数据库表的名称。
  3. 通过执行以下步骤来实现此方法:
    1. 获取对DataSource对象的引用。
    2. 通过使用键“ test.reset.sql.template”从属性文件( application.properties )中读取SQL模板。
    3. 打开数据库连接。
    4. 创建调用的SQL语句并调用它们。

DbTestUtil类的源代码如下所示:

import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

public final class DbTestUtil {

    private DbTestUtil() {}

    public static void resetAutoIncrementColumns(ApplicationContext applicationContext,
                                                 String... tableNames) throws SQLException {
        DataSource dataSource = applicationContext.getBean(DataSource.class);
        String resetSqlTemplate = getResetSqlTemplate(applicationContext);
        try (Connection dbConnection = dataSource.getConnection()) {
            //Create SQL statements that reset the auto increment columns and invoke 
            //the created SQL statements.
            for (String resetSqlArgument: tableNames) {
                try (Statement statement = dbConnection.createStatement()) {
                    String resetSql = String.format(resetSqlTemplate, resetSqlArgument);
                    statement.execute(resetSql);
                }
            }
        }
    }

    private static String getResetSqlTemplate(ApplicationContext applicationContext) {
        //Read the SQL template from the properties file
        Environment environment = applicationContext.getBean(Environment.class);
        return environment.getRequiredProperty("test.reset.sql.template");
    }
}
附加信息:

让我们继续前进,找出如何在集成测试中使用此类。

修复我们的集成测试

我们可以按照以下步骤修复集成测试:

  1. 将重置的SQL模板添加到示例应用程序的属性文件中。
  2. 在调用我们的测试方法之前,重置todos表的自动增量列( id )。

首先 ,我们必须将重置的SQL模板添加到示例应用程序的属性文件中。 此模板必须使用String类的format()方法支持的格式 。 因为我们的示例应用程序使用H2内存数据库,所以我们必须将以下SQL模板添加到属性文件中:

test.reset.sql.template=ALTER TABLE %s ALTER COLUMN id RESTART WITH 1
附加信息:

其次 ,在调用我们的测试方法之前,我们必须重置todos表的自动增量列( id )。 我们可以通过对ITTodoRepositoryTest类进行以下更改来做到这一点:

  1. 将包含我们示例应用程序配置的ApplicationContext对象注入到测试类中。
  2. 重置待办事项表的自动增量列。

固定集成测试类的源代码如下所示(突出显示了更改):

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.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
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 java.sql.SQLException;

@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 Long ID = 2L;
    private static final String DESCRIPTION = "description";
    private static final String TITLE = "title";
    private static final long VERSION = 0L;

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private TodoRepository repository;

    @Before
    public void setUp() throws SQLException {
        DbTestUtil.resetAutoIncrementColumns(applicationContext, "todos");
    }

    @Test
    @DatabaseSetup("no-todo-entries.xml")
    @ExpectedDatabase("save-todo-entry-with-title-and-description-expected.xml")
    public void save_WithTitleAndDescription_ShouldSaveTodoEntryToDatabase() {
        Todo todoEntry = Todo.getBuilder()
                .title(TITLE)
                .description(DESCRIPTION)
                .build();

        repository.save(todoEntry);
    }

    @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);
    }
}
附加信息:

当我们第二次运行集成测试时,它们通过了。

让我们继续并总结从这篇博客文章中学到的知识。

摘要

这个博客教会了我们三件事:

  • 如果我们不知道插入值自动生成的列中的值,我们将无法编写有用的集成测试。
  • 如果我们的应用程序没有很多集成测试,则使用@DirtiesContext注释可能是一个不错的选择。
  • 如果我们的应用程序有很多集成测试,则必须在调用每种测试方法之前重置自动增量列。

您可以从Github获得此博客文章的示例应用程序

翻译自: https://www.javacodegeeks.com/2014/11/spring-from-the-trenches-resetting-auto-increment-columns-before-each-test-method.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值