在Space 1st Apps的项目中,我们使用了基于Spring的单元测试 , 并结合DBUnit对数据库应用程序做单元测试。
其中,数据源我们使用了Oracle、MySql数据库,Oracle数据源只有一个数据库,而MySQL数据源是一个由多台MySQL数据库组成的分布式多数据源。
对于单数据源Single DataSource的单元测试用例,如果你需要在测试用例运行之前准备一些种子数据,那么,你只需要简单地继承抽象类:AbstractDataSourceTest,例如:
@ContextConfiguration(locations = { "/spring/space-apps-poke.xml" })
public class PokeInfoManagerTest extends AbstractDataSourceTest {
……
@Override
protected String getSeedXmlFile() {
return "/seeds/poke/pokeinfo.xml";
}
……
}
其中,@ContextConfiguration的参数locations的值,表示类路径下的Spring配置文件的位置,例如:/spring/space-apps-poke.xml,在开发时,你将该文件放在了src/conf文件下面,将该文件夹作为源文件夹使用,这样该文件夹下面的所有文件,就都会在类路径下面了,如下图所示:
必须实现的抽象方法:
protected abstract String getSeedXmlFile()
该方法实现,需要你返回类路径下面的测试种子数据XML文件的位置,测试种子数据文件统一放到src/conf.test下面,如下图所示:
在测试用例中,如果需要引用Spring的bean,只需像下面代码这样:
……
@Autowired
private PokeInfoManager pokeInfoManager;
……
一旦加上了@Autowired注解之后,id为“pokeInfoManager“的bean,就会自动注入到测试用例中。
测试方法,只需在普通的方法前面加上注解@Test,就可以被单元测试框架运行,例如:
@Test
public void getAllPokeInfoNotCached() {
memCachedTemplate.delete(AppsKeyAccessor.Memcached.SPACE_APPS_POKE);
List pokeInfoList = pokeInfoManager.getAllPokeInfo();
assertNotNull(pokeInfoList);
assertTrue(!pokeInfoList.isEmpty());
}
附上抽象超类的源码
@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class })
@ContextConfiguration(locations = { "/spring/space-memcached.xml",
"/spring/space-datasource.xml", "/spring/space-commons.xml",
"/spring/space-datasource-multi.xml" })
public abstract class AbstractDataSourceTest {
private Resource resource;
private IDatabaseTester tester;
@Autowired
@Qualifier("spaceOracleDS")
private DataSource dataSource;
/**
* Returns the test data seeds file in XML format
*/
protected abstract String getSeedXmlFile();
/**
* Creates a new IDatabaseTester.
* Default implementation returns a {@link DataSourceDatabaseTester}
* configured with the value returned from {@link getDataSource()}.
*/
protected IDatabaseTester newDatabaseTester() {
return new DataSourceDatabaseTester(getDataSource());
}
/**
*Returns the test DataSource.
*/
protected DataSource getDataSource() {
return this.dataSource;
}
/**
* Gets the IDatabaseTester for this testCase.
* If the IDatabaseTester is not set yet, this method calls
* newDatabaseTester() to obtain a new instance.
*
* @throws Exception
*/
protected IDatabaseTester getDatabaseTester() throws Exception {
if (this.tester == null) {
this.tester = newDatabaseTester();
}
return this.tester;
}
/**
* Returns the database operation executed in test setup. default is
* DatabaseOperation.REFRESH
*/
protected DatabaseOperation getSetUpOperation() throws Exception {
return DatabaseOperation.REFRESH;
}
/**
* Returns the database operation executed in test cleanup.default is
* DatabaseOperation.NONE
*/
protected DatabaseOperation getTearDownOperation() throws Exception {
return DatabaseOperation.NONE;
}
/**
* Returns data set resource in spring resource mode. eg: /seeds/xxx.xml If
* the Resource is not set yet, this method calls newDataSetResource() to
* obtain a new instance.
*/
private Resource getDataSetResource() {
if (this.resource == null) {
this.resource = newDataSetResource();
}
return this.resource;
}
/**
* Creates a new Resource.
* Default implementation returns a {@link ClassPathResource} configured
* with the value returned from {@link getSeedXmlFile()}.
*/
protected Resource newDataSetResource() {
return new ClassPathResource(getSeedXmlFile());
}
/**
* Returns the test dataset. default is FlatXmlDataSet
*/
private IDataSet getDataSet() throws Exception {
return new FlatXmlDataSet(getDataSetResource().getInputStream());
}
@Before
public void setUp() throws Exception {
final IDatabaseTester databaseTester = getDatabaseTester();
databaseTester.setSetUpOperation(getSetUpOperation());
databaseTester.setDataSet(getDataSet());
databaseTester.onSetup();
}
@After
public void tearDown() throws Exception {
final IDatabaseTester databaseTester = getDatabaseTester();
databaseTester.setTearDownOperation(getTearDownOperation());
databaseTester.setDataSet(getDataSet());
databaseTester.onTearDown();
tester = null;
}
}
如果你不需要测试种子数据,请不要继承AbstractDataSourceTest类,而是仅需继承Spring 测试框架自带的类:AbstractJUnit4SpringContextTests
如果你的测试用例想使用事务,譬如说,你不想每次运行测试用例,都把数据真实地插入到数据库中,你可以使用事务的自动回滚机制,这很简单,你只需要继承AbstractTransactionalDataSourceTest
@ContextConfiguration(locations = { "/spring/space-apps-album.xml" })
public class BoPhotoManagerTest extends AbstractTransactionalDataSourceTest {
……
@Override
protected String getSeedXmlFile() {
return "/seeds/admin/bophoto/bophoto.xml";
}
……
}
以上代码和无事务支持,需要测试种子数据测试用例比较类似,在此,不再累赘。
附上AbstractTransactionalDataSourceTest的源代码:
@TestExecutionListeners( { TransactionalTestExecutionListener.class })
@Transactional
@TransactionConfiguration(transactionManager = "jtaTransactionManager")
public abstract class AbstractTransactionalDataSourceTest extends
AbstractDataSourceTest {
}
其中,注解@TransactionConfiguration的参数transactionManager的值为jtaTransactionManager,是Spring的bean的Id
如果测试用例需要多数据源分布式数据库,大致类似,简单说一下
对于需要测试种子数据的,请继承AbstractDistributedDataSourceTest,附上该类源码:
@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class })
@ContextConfiguration(locations = { "/spring/space-memcached.xml",
"/spring/space-datasource.xml", "/spring/space-commons.xml",
"/spring/space-datasource-multi.xml" })
public abstract class AbstractDistributedDataSourceTest {
private Resource resource;
private IDistributedDatabaseTester tester;
@Autowired
@Qualifier("distributeDBDataSource")
private DataSource dataSource;
/**
* Returns the test data seeds file in XML format
*/
protected abstract String getSeedXmlFile();
/**
* Creates a new IDistributedDatabaseTester.
* Default implementation returns a
* {@link DistributedDataSourceDatabaseTester} configured with the value
* returned from {@link getDataSource()}.
*/
protected IDistributedDatabaseTester newDatabaseTester() {
return new DistributedDataSourceDatabaseTester(getDataSource());
}
/**
*Returns the test DataSource.
*/
protected DataSource getDataSource() {
return this.dataSource;
}
/**
* Gets the IDatabaseTester for this testCase.
* If the IDatabaseTester is not set yet, this method calls
* newDatabaseTester() to obtain a new instance.
*
* @throws Exception
*/
protected IDistributedDatabaseTester getDatabaseTester() throws Exception {
if (this.tester == null) {
this.tester = newDatabaseTester();
}
return this.tester;
}
/**
* Returns the database operation executed in test setup. default is
* DatabaseOperation.REFRESH
*/
protected DistributedDatabaseOperation getSetUpOperation() throws Exception {
return DistributedDatabaseOperation.REFRESH;
}
/**
* Returns the database operation executed in test cleanup.default is
* DatabaseOperation.NONE
*/
protected DistributedDatabaseOperation getTearDownOperation()
throws Exception {
return DistributedDatabaseOperation.NONE;
}
/**
* Returns data set resource in spring resource mode. eg: /seeds/xxx.xml If
* the Resource is not set yet, this method calls newDataSetResource() to
* obtain a new instance.
*/
private Resource getDataSetResource() {
if (this.resource == null) {
this.resource = newDataSetResource();
}
return this.resource;
}
/**
* Creates a new Resource.
* Default implementation returns a {@link ClassPathResource} configured
* with the value returned from {@link getSeedXmlFile()}.
*/
protected Resource newDataSetResource() {
return new ClassPathResource(getSeedXmlFile());
}
/**
* Returns the test dataset. default is FlatXmlDataSet
*/
private IExtDataSet getDataSet() throws Exception {
return new ExtXmlDataSet(getDataSetResource().getInputStream());
}
@Before
public void setUp() throws Exception {
final IDistributedDatabaseTester databaseTester = getDatabaseTester();
databaseTester.setSetUpOperation(getSetUpOperation());
databaseTester.setDataSet(getDataSet());
databaseTester.onSetup();
}
@After
public void tearDown() throws Exception {
final IDistributedDatabaseTester databaseTester = getDatabaseTester();
databaseTester.setTearDownOperation(getTearDownOperation());
databaseTester.setDataSet(getDataSet());
databaseTester.onTearDown();
tester = null;
}
}
如果需要事务回滚机制,请继承AbstractTransactionalDistributedDataSourceTest,附上该类源码
@TestExecutionListeners( { TransactionalTestExecutionListener.class })
@Transactional
@TransactionConfiguration(transactionManager = "jtaTransactionManager")
public abstract class AbstractTransactionalDistributedDataSourceTest extends
AbstractDistributedDataSourceTest {
}
好了,到目前为止,测试用例是写好了,但是测试种子数据,还没有准备,这个XML文件,看起来还蛮复杂的,大家看一下样本,
……
以上XML片段是一个样本,根节点是dataset,每个子节点表示表的名称,如FA_POKE_INFO,每个节点的属性如POKE_INFO_ID,表示数据库表的列名,每个节点的属性的值就是数据表对应列的值
OK,从头开始写这么一个文件,我觉得你一定会没有耐心,还是有一些工作量的。
还好,我写了几个工具类,你只需要简单地修改几个参数, 然后运行一下,上述的XML代码就会生成了,好,我们来看代码说话,由于我们使用了Oracle和MySQL数据库,所以我使用了模板方法模式,做了少许抽象,DatabaseExporter源码:
public abstract class DatabaseExporter {
private IDatabaseConnection databaseConnection;
private Connection jdbcConnection;
/**
* write DTD file
*
* @param dtdFileName
*
* @throws Exception
*/
protected void writeDataDtd(String dtdFileName) throws Exception {
IDataSet dataSet = getDatabaseConnection().createDataSet();
Writer out = new OutputStreamWriter(new FileOutputStream(dtdFileName));
FlatDtdWriter datasetWriter = new FlatDtdWriter(out);
datasetWriter.setContentModel(FlatDtdWriter.CHOICE);
datasetWriter.write(dataSet);
}
private IDatabaseConnection getDatabaseConnection() throws Exception {
if (this.databaseConnection == null) {
this.databaseConnection = newDatabaseConnection(getJdbcConnection());
}
return this.databaseConnection;
}
private Connection getJdbcConnection() throws Exception {
if (this.jdbcConnection == null) {
this.jdbcConnection = newJdbcConnection();
}
return this.jdbcConnection;
}
/**
* full database export
*
* @param fullXmlFileName
* @param isFlatXml
*
* @throws Exception
*/
protected void exportFullDataSet(String fullXmlFileName, boolean isFlatXml)
throws Exception {
IDataSet fullDataSet = getDatabaseConnection().createDataSet();
if (isFlatXml) {
FlatXmlDataSet.write(fullDataSet, new FileOutputStream(
fullXmlFileName));
} else {
XmlDataSet
.write(fullDataSet, new FileOutputStream(fullXmlFileName));
}
}
/**
* dependent tables database export: export table X and all tables that have
* a PK which is a FK on X, in the right order for insertion
*
* @param rootTableName
* @param dependentXmlFileName
* @param isFlatXml
* @throws Exception
*/
protected void exportDependentDataSet(String rootTableName,
String dependentXmlFileName, boolean isFlatXml) throws Exception {
String[] depTableNames = TablesDependencyHelper.getAllDependentTables(
getDatabaseConnection(), rootTableName);
IDataSet depDataset = getDatabaseConnection().createDataSet(
depTableNames);
if (isFlatXml) {
FlatXmlDataSet.write(depDataset, new FileOutputStream(
dependentXmlFileName));
} else {
XmlDataSet.write(depDataset, new FileOutputStream(
dependentXmlFileName));
}
}
/**
* partial database export
*
* @param tableName
* @param partialXmlFileName
* @param isFlatXml
* @throws Exception
*/
protected void exportPartialDataSet(String tableName,
String partialXmlFileName, boolean isFlatXml) throws Exception {
QueryDataSet partialDataSet = new QueryDataSet(getDatabaseConnection());
partialDataSet.addTable(tableName);
if (isFlatXml) {
FlatXmlDataSet.write(partialDataSet, new FileOutputStream(
partialXmlFileName));
} else {
XmlDataSet.write(partialDataSet, new FileOutputStream(
partialXmlFileName));
}
}
/**
* partial multiple database export
*
* @param tableName2Query
* @param partialXmlFileName
* @param isFlatXml
* @throws Exception
*/
protected void exportMultiPartialDataSet(
Map tableName2Query, String partialXmlFileName,
boolean isFlatXml) throws Exception {
QueryDataSet partialDataSet = new QueryDataSet(getDatabaseConnection());
Validate.notEmpty(tableName2Query,
"Please check tableName2Query, it must not be null or empty");
Map treeMap = new TreeMap(
tableName2Query);
Set> entrys = treeMap.entrySet();
for (Entry entry : entrys) {
if (StringUtils.isBlank(entry.getValue())) {
partialDataSet.addTable(entry.getKey());
} else {
partialDataSet.addTable(entry.getKey(), entry.getValue());
}
}
if (isFlatXml) {
FlatXmlDataSet.write(partialDataSet, new FileOutputStream(
partialXmlFileName));
} else {
XmlDataSet.write(partialDataSet, new FileOutputStream(
partialXmlFileName));
}
}
/**
* partial database export
*
* @param tableName
* @param query
* @param isFlatXml
* @throws Exception
*/
protected void exportPartialDataSet(String tableName, String query,
String partialXmlFileName, boolean isFlatXml) throws Exception {
QueryDataSet partialDataSet = new QueryDataSet(getDatabaseConnection());
partialDataSet.addTable(tableName, query);
if (isFlatXml) {
FlatXmlDataSet.write(partialDataSet, new FileOutputStream(
partialXmlFileName));
} else {
XmlDataSet.write(partialDataSet, new FileOutputStream(
partialXmlFileName));
}
}
protected abstract Connection newJdbcConnection() throws Exception;
protected abstract IDatabaseConnection newDatabaseConnection(
Connection jdbcConnection);
}
MySQL 数据导出工具源代码:
public class MysqlDatabaseExporter extends DatabaseExporter {
@Override
protected IDatabaseConnection newDatabaseConnection(
Connection jdbcConnection) {
return new MySqlConnection(jdbcConnection, null);
}
@Override
protected Connection newJdbcConnection() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
return DriverManager.getConnection(
"jdbc:mysql://10.2.225.151:3306/SNS", "sns", "alisoft");
}
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
MysqlDatabaseExporter exporter = new MysqlDatabaseExporter();
Map map = new HashMap();
map.put("FA_GIFT_RECEIVER",
"SELECT * FROM SNS.FA_GIFT_RECEIVER F limit 0,10;");
map.put("FA_GIFT_SENDER",
"SELECT * FROM SNS.FA_GIFT_SENDER F limit 0,10;");
map.put("FA_GIFT_STAT", "SELECT * FROM SNS.FA_GIFT_STAT F limit 0,10;");
exporter.exportMultiPartialDataSet(map, "statefullGift.xml", false);
// exporter.writeDataDtd("myFile.dtd");
// exporter.exportFullDataSet("full.xml", false);
// exporter.exportDependentDataSet("FA_PHOTO", "dependents.xml", false);
}
}
Oracle 数据导出工具源代码:
public class OracleDatabaseExporter extends DatabaseExporter {
@Override
protected IDatabaseConnection newDatabaseConnection(
Connection jdbcConnection) {
OracleConnection connection = new OracleConnection(jdbcConnection, null);
connection.getConfig().setFeature(
DatabaseConfig.FEATURE_SKIP_ORACLE_RECYCLEBIN_TABLES, true);
return connection;
}
@Override
protected Connection newJdbcConnection() throws Exception {
Class.forName("oracle.jdbc.driver.OracleDriver");
return DriverManager.getConnection(
"jdbc:oracle:thin:@10.2.224.34:1521:aepdb", "sns", "sns");
}
/**
* @param args
* @throws Exception
* @throws Exception
*/
public static void main(String[] args) throws Exception {
OracleDatabaseExporter exporter = new OracleDatabaseExporter();
exporter.exportPartialDataSet("FA_POKE_INFO", "partial.xml", false);
exporter.writeDataDtd("myFile.dtd");
// exporter.exportFullDataSet("full.xml", false);
exporter.exportDependentDataSet("BO_PHOTO", "dependents.xml", false);
}
}
代码很简单,如果你熟悉DBUnit,大家一看就会明白,我主要说说怎么个用法,对了,有一个小插曲,我们的多数据源分布式数据库,是根据用户的longId进行取模,然后选择不同的数据库的,这样为多数据源分布式数据库准备单元测试的种子数据,就会产生很大的障碍,我们必须能够让种子数据自动选择他们应该归属的数据库。
DBUnit的测试种子数据,大家使用最多的应该是FlatXMLFile,如上面的测试种子数据例子
在插入测试种子数据时,我们无法知道XML节点的哪个字段是作为选择多数据源的字段,我们使用isResourceId表示,平行化的结构很难表达这个ResourceId的意思。还好,DBUnit有一种非平行化的XML结构,如下XML代码所示:
POKE_ID SENDER_LONG_ID RECEIVER_LONG_ID POKE_CONTENT GMT_CREATE GMT_MODIFIED IS_DELETED
以上XML片段中,dataset表示根节点,table表示一个表,name属性表示table的名称,column表示table的一个列,中间的文本表示列名。
像以上这样,我们就可以在某个数据列上面表示哪个是ResoutceId,如上面例子中:isResourceId="true"的列,RECEIVIER_LONG_I,当然,默认的DBUnit是不支持这种扩展的,为了这个扩展,我费了很多的力气,感觉DBUnit的扩展性不是很好,至于扩展的代码,有很多,这里就不一一列举了。
说说上面的Exporter的使用方法,为了导出数据库中某个表,使用:
protected void exportPartialDataSet(String tableName,
String partialXmlFileName, boolean isFlatXml)
protected void exportPartialDataSet(String tableName, String query,
String partialXmlFileName, boolean isFlatXml) throws Exception
为了导出数据库中的多个表数据到一个XML文件中,使用:
protected void exportMultiPartialDataSet(
Map tableName2Query, String partialXmlFileName,
boolean isFlatXml) throws Exception
另外,还有两个方法,其一:
protected void exportFullDataSet(String fullXmlFileName, boolean isFlatXml)
throws Exception
该方法用于导出数据库中所有表到一个XML文件中,一般情况下,不需要使用该方法,尤其是数据量很大的时候,参数定义如下:
其二:
protected void writeDataDtd(String dtdFileName) throws Exception
该方法用于导出数据库表结构到一个DTD文件中,注意:该方法,可能对Oracle 10i 以上开启回收站功能的数据库,运行会失败,解决方案,就是禁用这个功能,需要DBA支持,遇到该问题时,你可以Google一下。
好了,说了这么多了,希望对你有用