摆脱困境:在每种测试方法之前重置自动增量列
當我們為將信息保存到數據庫的功能編寫集成測試時,我們必須驗證是否將正確的信息保存到數據庫。
如果我們的應用程序使用Spring Framework,則可以為此目的使用Spring Test DbUnit和DbUnit 。
但是,很難驗證是否在主鍵列中插入了正確的值,因為主鍵通常是通過使用自動增量或序列自動生成的。
這篇博客文章指出了與自動生成值的列相關的問題,并幫助我們解決了這一問題。
補充閱讀:
- 在標題為“ 從溝壑中彈跳:在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;@Autowiredprivate 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注釋是一個很好的解決方案。
補充閱讀:
- @DirtiesContext注釋的Javadoc
- @ DirtiesContext.ClassMode枚舉的Javadoc
其次 ,我們可以從數據集中省略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。
我們必須找到更好的解決方案。
尋找更好的解決方案
我們已經為我們的問題找到了兩種不同的解決方案,但是它們都產生了新的問題。 第三種解決方案基于以下思想:
如果我們不知道插入到自動增量列中的下一個值,則必須在調用每個測試方法之前重置自動增量列。
我們可以按照以下步驟進行操作:
讓我們弄臟雙手。
創建可以重置自動增量列的類
我們可以通過執行以下步驟來創建該類,該類可以重置指定數據庫表的自動增量列:
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 fileEnvironment environment = applicationContext.getBean(Environment.class);return environment.getRequiredProperty("test.reset.sql.template");} }附加信息:
- ApplicationContext接口的Javadoc
- DataSource接口的Javadoc
- 環境接口的Javadoc
- String.format()方法的Javadoc
讓我們繼續前進,找出如何在集成測試中使用此類。
修復我們的集成測試
我們可以按照以下步驟修復集成測試:
首先 ,我們必須將重置的SQL模板添加到示例應用程序的屬性文件中。 此模板必須使用String類的format()方法支持的格式 。 因為我們的示例應用程序使用H2內存數據庫,所以我們必須將以下SQL模板添加到屬性文件中:
test.reset.sql.template=ALTER TABLE %s ALTER COLUMN id RESTART WITH 1附加信息:
- 我們的示例應用程序的應用程序上下文配置類
- String.format()方法的Javadoc
- 重置H2中的自動增量
- 如何重置MySQL自動增量列
- PostgreSQL 9.3文檔:ALTER SEQUENCE
其次 ,在調用我們的測試方法之前,我們必須重置todos表的自動增量列( id )。 我們可以通過對ITTodoRepositoryTest類進行以下更改來做到這一點:
固定集成測試類的源代碼如下所示(突出顯示了更改):
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;@Autowiredprivate ApplicationContext applicationContext;@Autowiredprivate TodoRepository repository;@Beforepublic 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);} }附加信息:
- @Autowired批注的Javadoc
- ApplicationContext接口的Javadoc
- @Before注釋的Javadoc
當我們第二次運行集成測試時,它們通過了。
讓我們繼續并總結從這篇博客文章中學到的知識。
摘要
這個博客教會了我們三件事:
- 如果我們不知道插入值自動生成的列中的值,我們將無法編寫有用的集成測試。
- 如果我們的應用程序沒有很多集成測試,則使用@DirtiesContext注釋可能是一個不錯的選擇。
- 如果我們的應用程序有很多集成測試,則必須在調用每種測試方法之前重置自動增量列。
您可以從Github獲得此博客文章的示例應用程序 。
翻譯自: https://www.javacodegeeks.com/2014/11/spring-from-the-trenches-resetting-auto-increment-columns-before-each-test-method.html
總結
以上是生活随笔為你收集整理的摆脱困境:在每种测试方法之前重置自动增量列的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在带有组合框的值列表的下拉列表中显示显示
- 下一篇: 如何使用单例EJB,Ehcache和MB