日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

单元测试之DBUnit的使用以及原理剖析

發布時間:2024/4/11 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 单元测试之DBUnit的使用以及原理剖析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前面介紹了不少寫單元測試的內容,比方說Mockito和PowerMockito, JUnit 5,經常寫單元測試的想必對這些框架都比較熟悉。

這篇博客主要介紹下數據庫驅動測試框架–DbUnit(http://dbunit.sourceforge.net/), 主要從DbUnit的設計原理和實際使用來展開,這里的使用我又分為三個部分:

  • 基于spring-test-dbunit的使用
  • 基于dbunit本身api的使用
  • 在dbunit的基礎上整合了公司自己的jdbc框架完成的工具類
  • DBUnit 設計原理

    看過我之前關于單元測試的博客和熟悉單元測試的開發人員都知道,在寫單元測試時最重要的一點就是單元測試是要求可以反復執行驗證的。

    那么在我們對數據庫進行單元測試的時候,為了保證每次數據庫的單元測試都可以得到一個相同的結果,我們就不能直接使用數據庫里的數據來進行測試驗證,說不定什么時候數據就被別人修改了,而且我們的單測執行最好也不要對數據庫的數據有什么修改 — 很容易就想到的數據庫的事務特性。

    但是考慮到有的數據庫本身并不支持事務,比如MyISAM引擎,而由dbunit本身實現事務是比較復雜的,所以dbunit框架本身是沒有實現事務的

    dbunit的設計原理就是在執行測試用例之前,先備份數據庫,然后向數據庫中插入我們需要的初始化數據(準備數據),然后,在測試完畢后,清空表數據再將之前的備份的數據還原到數據庫,從而回溯到測試前的狀態。

    乍一看是不是也像是實現了一個"事務" ?但還是有兩個問題:

  • 如果在單測執行過程中遇到問題, 導致執行中斷,那么最后可能沒有正常還原數據,這樣的話就可能導致數據庫的數據丟失(所以無論單測執行成功還是失敗都記得一定要執行還原數據的代碼)
  • 單測執行過程中修改的數據在還原數據庫的時候是會有丟失的,不過因為是測試環境的數據,影響也不是很大
  • DBUnit 基本概念和流程

    基于DBUnit 單元測試的主要接口是IDataSet。IDataSet 數據集代表一個或多個表的數據。
    dbunit可以將數據庫的全部內容表示為IDataSet 實例。數據庫表可以用ITable 實例來表示。

    public interface IDataSet {/*** 從IDataSet獲取表名集合*/public String[] getTableNames() throws DataSetException;/*** 獲取數據庫指定表的元數據*/public ITableMetaData getTableMetaData(String tableName)throws DataSetException;/*** 獲取指定表*/public ITable getTable(String tableName) throws DataSetException;/*** 獲取所有的表集合*/public ITable[] getTables() throws DataSetException; }

    IDataSet 的實現有很多,每一個都對應一個不同的數據源或加載機制。最常用的幾種 IDataSet實現為:
    FlatXmlDataSet:數據的簡單平面文件 XML 表示
    QueryDataSet:用 SQL 查詢獲得的數據
    DatabaseDataSet:數據庫表本身內容的一種表示
    XlsDataSet :數據的excel表示

    我們使用DbUnit進行數據庫單元測試的流程如下:

  • 備份數據庫中的表數據
  • 準備好測試使用的初始化數據和預期的結果數據,一般用xml文件表示
  • 清空數據表并導入初始化數據。
  • 執行對應的測試方法,比較實際執行的返回結果與預期結果是否匹配
  • 使用備份文件還原表數據
  • DBUnit 使用

    spring 結合dbunit完成db測試

    dbunit本身并沒有提供事務支持的功能,但是spring是可以提供事務支持的,包括聲明式事務和程序控制事務。所以dbunit結合spring可以將上述單元測試的執行全都放在一個事務里,這樣就可以解決我上面提到的兩個問題

    如果結合spring使用dbunit進行單元測試,就需要引入dbunit和spring-test-dbunit兩個jar包

    <dependency><groupId>com.github.springtestdbunit</groupId><artifactId>spring-test-dbunit</artifactId><version>1.2.0</version><scope>test</scope></dependency><dependency><groupId>org.dbunit</groupId><artifactId>dbunit</artifactId><version>2.5.0</version><type>jar</type><scope>test</scope></dependency> @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = ServiceInitializer.class) @TestExecutionListeners({DependencyInjectionTestExecutionListener.class,DirtiesContextTestExecutionListener.class,TransactionalTestExecutionListener.class,DbUnitTestExecutionListener.class }) @DbUnitConfiguration(databaseConnection={"dataSource"}) @Transactional public class BaseTest {}

    因為我們使用@DbUnitConfiguration注解傳入了dataSource, 這樣在dbunit里獲取連接的時候得到就是從spring管理的數據源獲取的connection,這樣事務管理也可以由spring的聲明式事務托管。

    public class UserMapperDBUnitTest extends BaseTest {@Autowiredprivate UserMapper userMapper;@Test@DatabaseSetup("/dbunit/sampleData_initdata.xml",type = DatabaseOperation.CLEAN_INSERT)@ExpectedDatabase(value = "/dbunit/sampleData_result_insert.xml", assertionMode = DatabaseAssertionMode.NON_STRICT)public void testInsertSelective(){User user = new User();user.setId("2");user.setUserName("Tom");user.setAge(28);user.setBirthday("1993-03-21");user.setAddress("上海市浦東新區");userMapper.insertSelective(user);}}

    sampleData_initdata.xml :

    <?xml version="1.0" encoding="UTF-8"?> <dataset><user id="1" user_name="Bob" age = "20" birthday = "2000-01-02" address = "北京市大興區" /> </dataset>

    sampleData_result_insert.xml :

    <?xml version="1.0" encoding="UTF-8"?> <dataset><user id="1" user_name="Bob" age = "20" birthday = "2000-01-02" address = "北京市大興區" /><user id="2" user_name="Tom" age = "28" birthday = "1993-03-21" address = "上海市浦東新區" /> </dataset>

    @DatabaseSetup: 用于指定初始化數據庫的xml文件,以及初始化方式。 默認使用的是CLEAN_INSERT方式,也就是先清除數據庫的所有數據再插入準備的數據;如果表中的數據比較多,建議使用REFRESH方式,表示不會將原數據清空,而是直接對數據表中xml中存在的數據進行更新,不存在的就進行插入

    @ExpectedDatabase 執行完測試方法后,將數據庫中的數據查詢出來和xml中的數據進行比較
    注解參數query: 如果沒有則查詢所有的數據,否則按照指定的sql進行查詢
    參數 assertionMode: 支持兩種數據驗證方式:1)DatabaseAssertionMode.DEFAULT 要驗證所有的字段 2)DatabaseAssertionMode.NON_STRICT則支持只驗證部分字段(實際測試中NON_STRICT更為常用)

    使用dbunit原生api完成db測試

    上述spring-test-dbunit使用的前提是需要結合被spring管理的數據源, 因為公司有的舊項目是使用了自己開發的jdbc框架,其數據源無法直接獲取,也沒辦法使用上面簡單的注解方式

    所以自己使用了dbunit的API來編寫數據庫的單元測試,具體代碼如下:

    public class DBUnitConnection {private static IDatabaseConnection CONNECTION_INSTANCE = null;//創建DBUnit Connection,先創建數據源, 再從數據源中獲取到連接, 封裝成MySQLConnectionpublic static IDatabaseConnection getConnection() throws Exception {if (null == CONNECTION_INSTANCE) {//下面三行代碼主要是為了獲取數據庫連接,可以根據你在項目中實際獲取數據源和連接的方式調整XXDataSourceFactory factory = new XXDataSourceFactory();DataSource dataSource = factory.createDataSource();Connection connection = dataSource.getConnection();CONNECTION_INSTANCE = new MySqlConnection(connection,"userdb");}return CONNECTION_INSTANCE;}public void closeConnection() throws Exception {if (null != CONNECTION_INSTANCE) {if (!CONNECTION_INSTANCE.getConnection().isClosed()) {CONNECTION_INSTANCE.close();}CONNECTION_INSTANCE = null;}} } public class DbUnitUtil {//備份表數據public static void backupDatabase(String[] tables,File backupFile) throws Exception{QueryDataSet dataSet= new QueryDataSet(DBUnitConnection.getConnection());for(String _table:tables){dataSet.addTable(_table);}FlatXmlDataSet.write(dataSet, new FileOutputStream(backupFile));}//清空表數據,并導入測試數據public static void importTables(File dataFile) throws Exception{IDataSet dataSet=new FlatXmlDataSetBuilder().build(dataFile);DatabaseOperation.CLEAN_INSERT.execute(DBUnitConnection.getConnection(), dataSet);}//清空表數據,恢復備份數據public static void resumeDatabase(File backupFile) throws Exception{IDataSet dataSet= new FlatXmlDataSetBuilder().build(backupFile);DatabaseOperation.CLEAN_INSERT.execute(DBUnitConnection.getConnection(), dataSet);} } @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = {ServiceInitializer.class}) public class UserMapperDBUnitTest {@Autowiredprivate UserMapper userMapper;private static final String TABLE_NAME = "user";private static String path = "";@Beforepublic void init() throws Exception {path = UserMapperDBUnitTest .class.getClassLoader().getResource("").getPath()+"dbunit/backupAllData.xml";//備份數據表到path路徑下的xml文件DbUnitUtil.backupDatabase(new String[]{TABLE_NAME},new File(path));}@Afterpublic void down() throws Exception {//還原表數據DbUnitUtil.resumeDatabase(new File(path));}@Testpublic void testInsertOneRecord() throws Exception {String path = getClass().getClassLoader().getResource("").getPath()+"dbunit/sampleData_initdata.xml";//清空并導入初始化數據DbUnitUtil.importTables(new File(path));User user = new User();user.setId("2");user.setUserName("Tom");user.setAge(28);user.setBirthday("1993-03-21");user.setAddress("上海市浦東新區");userMapper.insertSelective(user);String resultFile = getClass().getClassLoader().getResource("").getPath()+"dbunit/sampleData_result_insert.xml";IDataSet dataSet = new FlatXmlDataSetBuilder().build(new File(resultFile));assertDataSet(TABLE_NAME, "SELECT id, user_name, age, birthday, address FROM user", dataSet);}

    雖然按照上面的方式可以實現數據庫的單元測試,但是會出現最早提到的兩個問題:

  • 執行過程中更新的數據會丟失
  • 執行失敗可能會導致原來測試數據庫的數據丟失
  • 所以還是需要一個"事務"幫助我們來解決上述問題。

    手動實現dbunit與事務的結合

    查看了下我們的jdbc框架,它本身也是有事務支持的,既支持聲明式事務,也支持編程式事務。我試著按照spring-test-dbunit和dbunit的使用方式來編寫測試方法,但是在執行的時候會報錯,提示使用事務注解的bean只能事務管理器來創建,所以最后我選擇了使用編程式事務來解決上述問題

    解決思路 :
    我的目的是將dbunit對數據庫的操作和應用代碼里對數據庫的操作放到一個事務里,那么首先二者需要處于一個連接中,我之前的代碼中直接從數據源創建新連接的方法是需要修改的;其次就是需要將對數據庫操作的代碼都放在編程式事務里

    為了方便使用,我將代碼進一步封裝,這樣在編寫測試用例的時候就可以只使用自定義注解和Rule來完成對數據庫的清除,還原等操作。

    修改后的代碼如下:

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DBUnitAnnotation {/*** Provides the locations of the datasets that will be used to reset the database.*/String setupFile() ;/*** Provides the locations of the datasets that will be used to test the database.*/String resultFile() default ""; } public class DBUnitUtils {/*** clean table and input init data to table* */public static void importTables(File dataFile) throws Exception{IDataSet dataSet = new FlatXmlDataSetBuilder().build(dataFile);//通過反射獲取項目中使用的connection實例DatabaseOperation.CLEAN_INSERT.execute(getConnectionInTransaction(), dataSet);}private static IDatabaseConnection CONNECTION_INSTANCE = null;/*** get the connection which is use in application* */public static IDatabaseConnection getConnectionInTransaction() throws Exception {// 這里是我根據公司的代碼寫的,你們可以按照自己項目的實際情況調整// 通過反射獲取事務管理器的transactionHolder靜態變量,從中獲取項目中使用的connection實例(因為公司的框架并沒有提供api讓我們可以在項目中獲取使用的連接實例)Field f = XXTransactionManager.class.getDeclaredField("transactionHolder");f.setAccessible(true);ThreadLocal<XXTransaction> transactionHolder = (ThreadLocal<XXTransaction>) f.get(null);XXTransaction transaction = transactionHolder.get();Connection connection = transaction.getConnection();CONNECTION_INSTANCE = new MySqlConnection(connection, "");return CONNECTION_INSTANCE;}/**** compare the database data with the expectedDatabase* @param expectedDataSet* @throws Exception*/public static void assertDataSet(IDataSet expectedDataSet) throws Exception {String[] tableNames = expectedDataSet.getTableNames();for (String tableName : tableNames) {//獲取dataSet的表元數據,得到對應的Column集合Column[] columns = expectedDataSet.getTable(tableName).getTableMetaData().getColumns();String queryField = "";for (int i = 0 ; i < columns.length ; i++) {queryField += columns[i].getColumnName();if (i != columns.length - 1) {queryField += " , ";}}String sql = "select " + queryField + " from " + tableName;QueryDataSet loadedDataSet = new QueryDataSet(DBUnitUtils.getConnectionInTransaction());loadedDataSet.addTable(tableName, sql);//從當前數據庫中查詢所有數據 并和預期的數據集進行比較ITable table1 = loadedDataSet.getTable(tableName);ITable table2 = expectedDataSet.getTable(tableName);Assert.assertEquals(table2.getRowCount(), table1.getRowCount());DefaultColumnFilter.includedColumnsTable(table1, table2.getTableMetaData().getColumns());Assertion.assertEquals(table2, table1);}} } public class DbUnitTransactionRule implements TestRule {@Overridepublic Statement apply(final Statement base, Description description) {if (description.getAnnotation(DBUnitAnnotation.class) == null) {return new Statement() {@Overridepublic void evaluate() throws Throwable {base.evaluate();}};}final DBUnitAnnotation dbUnitAnnotation = description.getAnnotation(DBUnitAnnotation.class);//如果有DBUnitAnnotation注解return new Statement() {@Overridepublic void evaluate() throws Throwable {try {//開啟事務String path = getClass().getClassLoader().getResource("").getPath() + dbUnitAnnotation.setupFile();DBUnitUtils.importTables(new File(path));base.evaluate();if (StringUtils.isNotEmpty(dbUnitAnnotation.resultFile())) {String resultFile = DBUnitUtils.class.getClassLoader().getResource("").getPath() + dbUnitAnnotation.resultFile();IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(new File(resultFile));DBUnitUtils.assertDataSet(expectedDataSet);}} catch (Throwable e) {e.printStackTrace();//如果原來的單測有異常,則拋出斷言失敗也就是測試用例執行失敗throw new AssertionError();} finally {//TODO 回滾,哪怕單測執行成功也要還原現場}}};} }

    使用的時候只需要加上DbUnitTransactionRule 和 @DBUnitAnnotation 注解就可以了,是不是很方便

    @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = {ServiceInitialier.class}) public class TRedPointRecordDaoDBUnitTest2 {@Autowiredprivate UserMapper userMapper;@Rulepublic DbUnitTransactionRule rule = new DbUnitTransactionRule();@Test@DBUnitAnnotation(setupFile = "dbunit/sampleData_initdata.xml", resultFile = "dbunit/sampleData_result_insert.xml")public void testInsertOneRecord() throws Exception {User user = new User();user.setId("2");user.setUserName("Tom");user.setAge(28);user.setBirthday("1993-03-21");user.setAddress("上海市浦東新區");userMapper.insertSelective(user);} }

    封裝的代碼有一些考慮的還不是很完整,比如不支持多個xml文件;在校驗數據的時候也沒有對兩個DataSet里的表做完全的相等判斷

    我本來是想寫成兩個注解,但是在測試的時候發現Rule的Description只能拿到兩個注解,所以我就把兩個注解定義成一個了 – 目前還沒找到原因,如果有讀者知道這個問題的答案歡迎在評論區分享下

    總結

    基本關于DBUnit的介紹就到這里了。

    使用DBUnit進行數據庫的單元測試,最好是可以結合事務來執行,這樣可以避免出現測試數據沒有被正常還原或者丟失執行過程中更新的數據的問題。

    基本思路就是 開啟事務 --> 清空表數據 --> 插入初始化數據 --> 執行測試方法 --> 查詢表數據,比較預期結果和執行結果是否一致 --> 回滾事務(無論測試方法是否正確執行,最后都需要回滾)

    最后的一部分是我基于工作中整合dbunit和內部的jdbc框架的需要,因為不同的jdbc框架獲得connection的方式不一樣(甚至有的框架可能也支持類似spring-test-dbunit的聲明式事務的寫法),所以我只是寫了自己項目中的代碼實現,希望對有同樣需求的開發者可以提供一些思路。

    參考資料:

    JUnit單元測試6—@Rule注解

    總結

    以上是生活随笔為你收集整理的单元测试之DBUnit的使用以及原理剖析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。