使用DBUnit做单元测试

标签: dbunit 单元测试 | 发表时间:2013-11-25 10:35 | 作者:顺其自然EVO
出处:http://www.blogjava.net/qileilove/
 DBUnit是一个方便的数据准备工具, 方便于我们做 单元测试的时候准备数据, 它的数据准备是基于XML格式的, 如下:
<?xmlversion='1.0'encoding='UTF-8'?>
<dataset>
<YourTableName_1Field_1="1"Field_2="f2"Field_3="f3"/>
<YourTableName_1Field_1="2"Field_2="f2_1"Field_3="f3_1"/>
<YourTableName_2Field_1="1"Field_2="2"/>
</dataset>
  DBUnit的一个XML数据文件中,可以同时放多个表的数据,并且可以方便的把上面XML中准备的数据插入倒 数据库中. 只需要使用下面简单的代码就可以做到:
protected ReplacementDataSet createDataSet(InputStream is) throws Exception {
return new ReplacementDataSet(new FlatXmlDataSetBuilder().build(is));
}
ReplacementDataSet createDataSet = createDataSet(Thread.currentThread().getContextClassLoader().getResourceAsStream("data.xml"));
DatabaseOperation.INSERT.execute(iconn, createDataSet);
   注:准备这处XML数据文件时,一定要把同一个表中字段数最多的记录放在前面,因为DBUnit在根据数据XML文件准备表的元数据字段的时候,是以当前表的第一记录为主的。如下面这个XML文件:
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<YourTableName_1 Field_1="1" Field_2="f2"/>
<YourTableName_1 Field_1="2" Field_2="f2_1" Field_3="f3_1"/>
<YourTableName_2 Field_1="1" Field_2="2"/>
</dataset>
  Table YourTableName_1有三个字段,但是第三个字段Field_3是允许为空的,所以上面的XML是可以这样写的,并且DBUnit在执行插入的时候也不会报错,但是这里会出现一个问题,因为DBUnit在第一次分析到table YourTableName_1时,第一条记录只有两个字段,因而它在记录table YourTableName_1的字段的时候,就只会记录两个到这个表的元数据信息,因而在对在对后面记录进行数据处理的时候,只会取后面记录的两个字段,而第三个字段是不会被插入到数据库中。解决这个问题很简单,把YourTableName_1的第二条记录和第一条记录给换一下位置就好了。 同理,在数据result数据文件的时候,也要遵循这样的规则,否则会得不到想要的结果的。这是经验总结。
 你可能会担心如果是要准备的数据比较多是不是会比较麻烦,如上百条的数据准备,这个可以不用担心,因为使用DBUnit可以方便的从数据库中导出数据到指定的文件中,然后供我们使用,使用以下这个方法就可以导出数据:
/**
* Export data for the table names by the given IDatabaseConnection into the resultFile.<br>
* The export data will be DBUnit format.
*
* @param conn
* @param tableNameList
* @param resultFile
* @throws SQLException
* @throws DatabaseUnitException
* @throws FileNotFoundException
* @throws IOException
*/
public void exportData(IDatabaseConnection iconn, List<String> tableNameList, String resultFile) throws SQLException, DatabaseUnitException, FileNotFoundException, IOException {
QueryDataSet dataSet = null;
if (iconn == null) {
return;
}
if (tableNameList == null || tableNameList.size() == 0) {
return;
}
try {
dataSet = new QueryDataSet(iconn);
for (String tableName : tableNameList) {
dataSet.addTable(tableName);
}
} finally {
if (dataSet != null) {
FlatXmlDataSet.write(dataSet, new FileOutputStream(resultFile));
}
}
}
  DBUnit的另一个非常有用的功能,就是对执行结果进行比较,这样可以直接得到执行结果是否正确。 操作方式是准备一个执行期待结果的XML文件,再准备一条从数据库查询结果的SQL。这里有一个经验非常重要,那就是用于查询的执行结果的SQL文件,最好是加上某个关键字段的ORDER BY语句,否则可能会因为记录的顺序而比较失败,因为DBUnit是把查询出来的结果和准备的结果进行一一对应的比较。当然,既然SQL查询都加上了排序,那我们的结果XML文件,也应该是根据关键字段排好序的结果的,否则也会因为记录的顺序问题而比较失败。
 你可能会担心如果是要准备的数据比较多是不是会比较麻烦,如上百条的数据准备,这个可以不用担心,因为使用DBUnit可以方便的从数据库中导出数据到指定的文件中,然后供我们使用,使用以下这个方法就可以导出数据:
/**
* Export data for the table names by the given IDatabaseConnection into the resultFile.<br>
* The export data will be DBUnit format.
*
* @param conn
* @param tableNameList
* @param resultFile
* @throws SQLException
* @throws DatabaseUnitException
* @throws FileNotFoundException
* @throws IOException
*/
public void exportData(IDatabaseConnection iconn, List<String> tableNameList, String resultFile) throws SQLException, DatabaseUnitException, FileNotFoundException, IOException {
QueryDataSet dataSet = null;
if (iconn == null) {
return;
}
if (tableNameList == null || tableNameList.size() == 0) {
return;
}
try {
dataSet = new QueryDataSet(iconn);
for (String tableName : tableNameList) {
dataSet.addTable(tableName);
}
} finally {
if (dataSet != null) {
FlatXmlDataSet.write(dataSet, new FileOutputStream(resultFile));
}
}
}
  DBUnit的另一个非常有用的功能,就是对执行结果进行比较,这样可以直接得到执行结果是否正确。 操作方式是准备一个执行期待结果的XML文件,再准备一条从数据库查询结果的SQL。这里有一个经验非常重要,那就是用于查询的执行结果的SQL文件,最好是加上某个关键字段的ORDER BY语句,否则可能会因为记录的顺序而比较失败,因为DBUnit是把查询出来的结果和准备的结果进行一一对应的比较。当然,既然SQL查询都加上了排序,那我们的结果XML文件,也应该是根据关键字段排好序的结果的,否则也会因为记录的顺序问题而比较失败。

  上面的是热身,该来点实际的东西了, 弄个真实的实例来看看,下面是一个用于DBUnit测试的抽象类:
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import junit.framework.Assert;
import org.dbunit.Assertion;
import org.dbunit.DatabaseUnitException;
import org.dbunit.IDatabaseTester;
import org.dbunit.JdbcDatabaseTester;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.Column;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.filter.DefaultColumnFilter;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.junit.runner.RunWith;
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.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:/spring.xml" })
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class })
@Transactional
public abstract class BasedTestCase {
protected static Properties properties = new Properties();
static {
try {
/**
* The DatabaseConfig.properties stores the database configuration information. It's like this: <br>
* driverClass=oracle.jdbc.OracleDriver<br>
* db_inst=jdbc:oracle:thin:@1.1.1.1:1521:schema<br>
* db_user=username<br>
* db_pwd=password<br>
*/
properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("DatabaseConfig.properties"));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* This abstract is used for prepare data before do the real method call.
*
* @param iconn
* @throws Exception
*/
protected abstract void prepareData(IDatabaseConnection iconn) throws Exception;
/**
* Execute one sql
*
* @param iconn
* @param sql
* @throws Exception
*/
protected void execSql(IDatabaseConnection iconn, String sql) throws Exception {
Connection con = iconn.getConnection();
Statement stmt = con.createStatement();
try {
stmt.execute(sql);
} finally {
if (stmt != null) {
stmt.close();
}
}
}
/**
* Get IDatabaseConnection connection
*
* @return
* @throws Exception
*/
protected IDatabaseConnection getIDatabaseConnection() throws Exception {
String db_inst = "", db_user = "", db_pwd = "", driverClass = "";
//The default is commit the record
db_user = properties.getProperty("db_user");
db_inst = properties.getProperty("db_inst");
db_pwd = properties.getProperty("db_pwd");
driverClass = properties.getProperty("driverClass");
IDatabaseConnection iconn = null;
IDatabaseTester databaseTester;
databaseTester = new JdbcDatabaseTester(driverClass, db_inst, db_user, db_pwd);
iconn = databaseTester.getConnection();
return iconn;
}
/**
* This is used to assert the data from table and the expected data set. If all of the them has the same records, then the assert is true.
*
* @param tableName
* @param sql
* @param expectedDataSet
* @param iconn
* @throws Exception
*/
protected void assertDataSet(String tableName, String sql, IDataSet expectedDataSet, IDatabaseConnection iconn) throws Exception {
printDataAsXml(iconn, tableName, sql);
QueryDataSet loadedDataSet = new QueryDataSet(iconn);
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);
}
/**
* Create the data set by input stream which read from the dbunit xml data file.
*
* @param is
* @return
* @throws Exception
*/
protected ReplacementDataSet createDataSet(InputStream is) throws Exception {
return new ReplacementDataSet(new FlatXmlDataSetBuilder().build(is));
}
/**
* Convert the data in the ITable to List
*
* @param table
* @return
* @throws Exception
*/
private List<Map<?, ?>> getDataFromTable(ITable table) throws Exception {
List<Map<?, ?>> ret = new ArrayList<Map<?, ?>>();
int count_table = table.getRowCount();
if (count_table > 0) {
Column[] columns = table.getTableMetaData().getColumns();
for (int i = 0; i < count_table; i++) {
Map<String, Object> map = new TreeMap<String, Object>();
for (Column column : columns) {
map.put(column.getColumnName().toUpperCase(), table.getValue(i, column.getColumnName()));
}
ret.add(map);
}
}
return ret;
}
/**
* Get data by the SQL and table name, then convert the data in the ITable to List
*
* @param iconn
* @param tableName
* @param sql
* @return
* @throws Exception
*/
protected List<Map<?, ?>> getTableDataFromSql(IDatabaseConnection iconn, String tableName, String sql) throws Exception {
ITable table = iconn.createQueryTable(tableName, sql);
return getDataFromTable(table);
}
/**
* Get data by the SQL and table name, then convert the data in the ITable to List. And the print the data as xml data format.
*
* @param iconn
* @param tableName
* @param sql
* @throws Exception
*/
protected void printDataAsXml(IDatabaseConnection iconn, String tableName, String sql) throws Exception {
List<Map<?, ?>> datas = getTableDataFromSql(iconn, tableName, sql);
StringBuffer sb;
for (Map<?, ?> data : datas) {
sb = new StringBuffer();
sb.append("<" + tableName.toUpperCase() + " ");
for (Object o : data.keySet()) {
sb.append(o + "=\"" + data.get(o) + "\" ");
}
sb.append("/>");
System.out.println(sb.toString());
}
}
/**
* Export data for the table names by the given Connection into the resultFile.<br>
* The export data will be DBUnit format.
*
* @param conn
* @param tableNameList
* @param resultFile
* @throws SQLException
* @throws DatabaseUnitException
* @throws FileNotFoundException
* @throws IOException
*/
public void exportData(Connection conn, List<String> tableNameList, String resultFile) throws SQLException, DatabaseUnitException, FileNotFoundException, IOException {
if (conn == null) {
return;
}
IDatabaseConnection iconn = new DatabaseConnection(conn);
exportData(iconn, tableNameList, resultFile);
}
/**
* Export data for the table names by the given IDatabaseConnection into the resultFile.<br>
* The export data will be DBUnit format.
*
* @param conn
* @param tableNameList
* @param resultFile
* @throws SQLException
* @throws DatabaseUnitException
* @throws FileNotFoundException
* @throws IOException
*/
public void exportData(IDatabaseConnection iconn, List<String> tableNameList, String resultFile) throws SQLException, DatabaseUnitException, FileNotFoundException, IOException {
QueryDataSet dataSet = null;
if (iconn == null) {
return;
}
if (tableNameList == null || tableNameList.size() == 0) {
return;
}
try {
dataSet = new QueryDataSet(iconn);
for (String tableName : tableNameList) {
dataSet.addTable(tableName);
}
} finally {
if (dataSet != null) {
FlatXmlDataSet.write(dataSet, new FileOutputStream(resultFile));
}
}
}
}


这个抽象类里面有实用插入数据、导出数据及验证数据的实现,也包括了数据库连接的准备,该类里面包含了一个抽象方法prepareData,因为任何使用DBUnit做单元测试的,应该是少不了数据准备这么一个过程,否则就只能够使用数据库中的现成数据,这样的单元测试是不靠谱的,因为数据库中的数据随时可能发生变化,这里的抽象方法prepareData就相当于在提醒写单元测试的人,不要忘了准备单元测试要用的数据。
  根据上面的思路,准备一个用于测试的Table:
create table YouTableName_1(
filed_1 int,
filed_2 varchar2(50),
filed_3 varchar2(50)
)
  用于测试的数据:
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<YouTableName_1 Filed_1="1" Filed_2="f2" Filed_3="f3"/>
<YouTableName_1 Filed_1="2" Filed_2="f2_1" Filed_3="f3_1"/>
</dataset>
  用于验证测试结果的数据:
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<YouTableName_1 Filed_1="1" Filed_2="a" Filed_3="a1"/>
<YouTableName_1 Filed_1="2" Filed_2="b" Filed_3="b1"/>
</dataset>
  我们这个测试非常简单,就是把Filed_1为1的字段中Filed_2和Filed_3的字段的值分别设为"a"和"a1",把Filed_1为2的字段中Filed_2和Filed_3的字段的值分别设为"b"和"b1",做完测试后,数据库中是不会插入我们的单元测试的数据的。
  下面这个类UpdateTest用于更新数据:
import java.sql.Connection;
import java.sql.SQLException;
public class UpdateTest {
private static boolean commit = true;
public UpdateTest() {
}
private void commit(Connection conn) throws SQLException {
if (commit) {
conn.commit();
}
}
public void updateFiled(Connection conn) throws SQLException {
conn.createStatement().execute("update YouTableName_1 set filed_2='a',filed_3='a1' where filed_1=1");
conn.createStatement().execute("update YouTableName_1 set filed_2='b',filed_3='b1' where filed_1=2");
commit(conn);
}
}
 下面这个MyTest类,就是用于单元测试的类:
package com.ubs.cre.tools.datatool.ipl;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import junit.framework.Assert;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.junit.Test;
import com.ubs.cre.BasedTestCase;
public class MyTest extends BasedTestCase {
@Test
public void testSend() throws IOException, SQLException {
Connection conn = null;
Boolean result = Boolean.FALSE;
IDatabaseConnection iconn = null;
try {
//Get DBUnit conneciton
iconn = getIDatabaseConnection();
//Get database connection
conn = iconn.getConnection();
//Set auto commit false
conn.setAutoCommit(false);
//prepare data
prepareData(iconn);
//use reflect to set the commit field to false
Class<UpdateTest> clazz = UpdateTest.class;
Field commitField = clazz.getDeclaredField("commit");
commitField.setAccessible(true);
commitField.setBoolean(clazz, false);
//call the method updateFiled
Method method = clazz.getDeclaredMethod("updateFiled", java.sql.Connection.class);
method.setAccessible(true);
//Before call the method, the clazz must be get an new install, because the called method "updateFiled" is not static.<br>
//If the called method is static, it will not need newInstance.
method.invoke(clazz.newInstance(), conn);
// get result data set by result xml file
ReplacementDataSet dataload_result = createDataSet(Thread.currentThread().getContextClassLoader().getResourceAsStream("MyTest_Result.xml"));
// compare the data which get from database and the expected result file
assertDataSet("YouTableName_1", "select filed_1,filed_2,filed_3 from YouTableName_1 order by filed_1", dataload_result, iconn);
} catch (Exception e) {
e.printStackTrace();
Assert.assertTrue(result);
} finally {
if (conn != null) {
conn.rollback();
conn.close();
}
}
}
protected void prepareData(IDatabaseConnection iconn) throws Exception {
//Remove the data from table YouTableName_1
execSql(iconn, "delete from YouTableName_1");
//INSERT TEST DATA
ReplacementDataSet createDataSet = createDataSet(Thread.currentThread().getContextClassLoader().getResourceAsStream("MyTest.xml"));
DatabaseOperation.INSERT.execute(iconn, createDataSet);
}
}
  好了,示例完了,非常的简单,也非常的清晰,不过美中不足就是和DBUnit的代码耦合度太高了,这过对于我们使用习惯了Spring的人来说,看起来是非常别扭的,后面我会写另外一个与Spring集成的、完全非侵入式的测试实现,等着吧。


顺其自然EVO 2013-11-25 10:35 发表评论

相关 [dbunit 单元测试] 推荐:

使用DBUnit做单元测试

- - BlogJava-qileilove
 DBUnit是一个方便的数据准备工具, 方便于我们做. 单元测试的时候准备数据, 它的数据准备是基于XML格式的, 如下:.   DBUnit的一个XML数据文件中,可以同时放多个表的数据,并且可以方便的把上面XML中准备的数据插入倒. 只需要使用下面简单的代码就可以做到:.    注:准备这处XML数据文件时,一定要把同一个表中字段数最多的记录放在前面,因为DBUnit在根据数据XML文件准备表的元数据字段的时候,是以当前表的第一记录为主的.

Android单元测试

- - CSDN博客推荐文章
    单元测试不管对于初学编程还是已经工作了很久的开发者来说,都不乐意花时间去写认为没用的代码进行测试,只要交给测试人员就行了,虽然这样也能把软件改出来,但也许你要花上几倍的时间去修改问题,如果在开发的过程中花点时间去写单元测试代码,把尽可能出问题的地方都测试一遍,把问题扼杀在最开始的地方,这样你就不必为后来找问题出处而烦恼.

Hadoop之MapReduce单元测试

- - ITeye博客
通常情况下,我们需要用小数据集来单元测试我们写好的map函数和reduce函数. 而一般我们可以使用Mockito框架来模拟OutputCollector对象(Hadoop版本号小于0.20.0)和Context对象(大于等于0.20.0). 下面是一个简单的WordCount例子:(使用的是新API).

springboot单元测试技术

- - 海思
整个软件交付过程中,单元测试阶段是一个能够最早发现问题,并且可以重复回归问题的阶段,在单元测试阶段做的测试越充分,软件质量就越能得到保证. 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/master/spring-unit-test.

“单元测试要做多细?”

- - 酷壳 - CoolShell.cn
这篇文章主要来源是StackOverflow上的一个回答——“ How deep are your unit tests?”. 一个有13.8K的分的人( John Nolan)问了个关于TDD的问题,他说——. “TDD需要花时间写测试,而我们一般多少会写一些代码,而第一个测试是测试我的构造函数有没有把这个类的变量都设置对了,这会不会太过分了.

文章: Android中的单元测试

- - InfoQ cn
随着Agile的普及,以及开发人员对测试重要性的认识逐步加深,单元测试已经成了越来越多软件项目开发中不可缺少的一部分. 无论项目是不是采用TDD的形式来进行开发,单元测试都能够为项目的修改和重构提供一定的保障. 有奖参与:天翼伦敦会,上传应用,为中国队加油. QClub七月技术沙龙(太原/北京/上海/厦门/西安 7月21/28/29日 免费报名中.

迈出单元测试的第一步

- - 酷勤网-挖经验 [expanded by feedex.net]
单元测试不仅是软件行业的最佳实践,在敏捷方法的推动下,它也成为了可持续软件生产的支柱. 年度敏捷调查,70%的参与者会对他们的代码进行单元测试. 单元测试和其他敏捷实践密切相关,所以开始编写测试是组织向敏捷转型的踏脚石. 我将在本文介绍符合要求的小技巧,以及在开发周期里进行单元测试的步骤. 没有自动化,单元测试的习惯也不会持续太久.

iOS开发进阶之单元测试

- - 博客园_首页
本文侧重讲述如何在iOS程序的开发过程中使用单元测试. 使用Xcode自带的OCUnit作为测试框架. 单元测试作为敏捷开发实践的组成之一,其目的是提高软件开发的效率,维持代码的健康性. 其目标是证明软件能够正常运行,而不是发现bug(发现bug这一目的与开发成本是正相关的,虽然发现bug是保证软件质量的一种手段,但是很显然这与降低软件开发成本这一目的背道而驰).

Java 单元测试利器之 Junit

- - 博客园_首页
          因为工作和学习的需要,在代码中查错的时候,第一步就是想知道这个错误具体发生在一个位置,进行一个准确的定位. 而这个定位的工作交给谁来做了呢. 不难猜出也就是这篇博客的主题---Junit. junit是一个开源的框架,也是java这一块的测试工具之一. 想了解详细请上官网,下面用代码来跟大家解释.

单元测试里的 5 个错误

- - ITeye博客
当我第一次听说可以使用框架比如JUnit来进行单元测试的时候,我惊叹这真是一个简单而强大的概念. 它取代了随机测试,使你可以保存你的测试代码,并按照需要随时运行它们. 按照我的理解,关于单元测试并没有多少产生误解的可能. 但是过去的几年中,我确实见过几种或多或少不太正确的单元测试使用方式. 如果跟协作逻辑代码分离开来,那么算法逻辑是最容易测试的.