Spring实战:为测试方法重置自增列

当我们为往数据库中保存信息的方法写集成测试的时候,我们必须验证是否保存了正确的信息。如果程序使用了Spring框架,我们可以使用Spring Test DbUnit 和 DbUnit。然而,验证主键列的值是否正确仍然非常困难。因为主键一般是用自增列自动生成的。这篇博文首先说明关于自动生成列的问题,然后提出解决办法。

我们不能断言未知

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

  • 第一个测试验证在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数据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-with-title-and-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

原因是todo表的id列是自增列,而调用它的集成测试首先”取”id 1。在第二次进行集成测试的时候,值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 id="1" description="[null]" title="title" version="0"/>
</dataset>

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

但是如果每个todo条目都有多个标签,就可能有问题了。假设我们要写一个集成测试往数据库插入两条新的todo条目,然后建立DbUnit数据集来确保:

  • 标题为”title one”的条目有一个叫做“tag one”的标签。
  • 标题为”title two”的条目有一个叫做“tag two”的标签。

看起来像这样:

<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数据集,因为我们不知道存入数据库的todo条目的id.

必须找一个更好的方案。

寻找更好的方案

我们找到了两种解决问题的方案,但是它们都带来了新的问题。基于下面的想法,我们有第三种解决方案:

如果我们不知道插入自增列的下一个值,我们必须在每个测试方法执行之前重置自增列。

可以用下面的步骤:

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

让我们开始吧。

创建一个可以重置自增列的类

我们可以用下面的步骤来创建一个可以重置指定数据表自增列的类:

  1. 创建一个叫DbTestUtil 的final类,添加私有的构造方法来避免实例化。
  2. 给它添加一个public static void resetAutoIncrementColumns() 方法。这个方法有两个参数:
    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. 重置todos表的自增列。

改好的集成测试源代码如下所示(修改高亮显示):

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下载例程

补充阅读

  • 测试用的程序在另一篇博文中已经描述过了: 实战Spring:在DbUnit数据集中使用空值。建议你首先阅读,在本文中将不再重复其内容。
  • 如果你不知道怎么给储存库写集成测试,你应该阅读这篇博文:Spring数据持久化导论之集成测试。它解释了应该如何为Spring数据持久化库写集成测试,对于其他基于Spring使用关系型数据库的代码,你也可以用同样的方法。

关于作者 Petri Kainulainen

Petri对软件开发和持续改进很有热情。他是Spring框架的软件开发专家,并且是<Spring Data>一书的作者。

原文链接: javacodegeeks 翻译: ImportNew.com - 冲哥Bob
译文链接: http://www.importnew.com/14129.html
[ 转载请保留原文出处、译者和译文链接。]

关于作者: 冲哥Bob

(新浪微博:@冲哥BOB

查看冲哥Bob的更多文章 >>



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部