Spring實戰:為測試方法重置自增列

jopen 9年前發布 | 21K 次閱讀 Spring JEE框架

當我們為往數據庫中保存信息的方法寫集成測試的時候,我們必須驗證是否保存了正確的信息。如果程序使用了Spring框架,我們可以使用Spring Test DbUnitDbUnit。然而,驗證主鍵列的值是否正確仍然非常困難。因為主鍵一般是用自增列自動生成的。這篇博文首先說明關于自動生成列的問題,然后提出解決辦法。

我們不能斷言未知

讓我們先給CrudRepository接口的save()方法寫兩個集成測試。這些測試如下描述:

  • 第一個測試驗證在Todo對象的標題和描述都已設置的情況下,數據庫里保存了正確的信息。
  • 第二個測試驗證在只有標題已設置的情況下,數據庫里保存了正確的信息。
  • </ul>

    兩個測試使用相同的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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!