最近在对unitils进行扩展, 主要是数据库这块儿的内容, 对, 就是dbunit, dbunit给我的感觉是扩展性太差了, 很多类的构造函数采用包可见, 抽象类的抽象方法包可见, 根本没办法继承复写某些方法, 可定制性和unitils比起来也差的不是一点点, 根本就是一个封闭的系统. 导致很多代码不得不大段的拷贝代码来满足自身的需要.
我这里采用了excel格式来构造测试数据, 目前发现的dbunit(使用版本为2.4.6)的几个问题:
- 有些大数字会采用科学计数法来表示, 导致在解析的时候数据不正确
- dbunit内部对日期时间的处理会用到TimeZone这个东东, 这个在国际化方面应该是有价值的, 但是在我们的测试中却会导致时间与格林威治时间做8个小时的偏移转换
- 对excel中的空行没有进行处理(比如excel本来只有两行数据, 但是不知什么原因会存在一些空行, 导致在插入数据库的时候会有Null相关的错误)
- 对测试数据不仅涉及到根据主键清理, 有时候还需要根据一些特殊的字段值进行清理, 比如唯一键, 而这个需要进行扩展, 而XlsTable的包可见, 基本上必须另外实现一套(TMD,恨得让人直咬牙).
针对以上问题的解决方案:
科学计数法问题
这里需要对XlsTable中的getValue()方法下进行处理, 具体代码如下:
static final Pattern pattern = Pattern.compile("[eE]");
private BigDecimal toBigDecimal(double cellValue) {
String resultString = String.valueOf(cellValue);
// 针对科学计数法的处理(对于小数位数精确到5位)
if (pattern.matcher(resultString).find()) {
DecimalFormat format = new DecimalFormat("#####.#####");
resultString = format.format(cellValue);
}
if (resultString.endsWith(".0")) {
resultString = resultString.substring(0, resultString.length() - 2);
}
BigDecimal result = new BigDecimal(resultString);
return result;
}
这里暂定精确到小数5位, 有没有更好的解决方案?
日期时间问题
在某个地方对TimeZone做个初始化, 具体原理懒得去探究了, 反正我这样解决了问题
// 由于dbunit对excel的时间处理会使用TimeZone.getOffset()做一个偏移转换, 这里需要设置一下
TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
空行的问题
这个需要实现一个IBatchStatement, 在批量处理数据的时候, 如果遇到空行应该跳过, dbunit有一个自己的实现:BatchStatement, 日!把构造函数声明为包可见, 让你无从继承改写. 无奈copy一些代码重写了一个. 另外还需要重写一个PreparedStatementFactory, 用来创建自己的那个IBatchStatement实现类.
public class TPreparedBatchStatement implements IPreparedBatchStatement {
boolean notAllNull; // 所有参数是否为null
boolean noParameter = true; // 是否指定参数
private int _index;
...
public void close() throws SQLException {
...
}
public void addValue(Object value, DataType dataType)
throws TypeCastException, SQLException {
logger.debug("addValue(value={}, dataType={}) - start", value, dataType);
noParameter = false;
// Special NULL handling
if (value == null || value == ITable.NO_VALUE) {
_statement.setNull(++_index, dataType.getSqlType());
return;
}
notAllNull = true;
dataType.setSqlValue(value, ++_index, _statement);
}
public void addBatch() throws SQLException {
logger.debug("addBatch() - start");
// 没有参数, 或者有参数, 但是参数不全为null
if (noParameter || (!noParameter && notAllNull)) {
_statement.addBatch();
notAllNull = false;
noParameter = true;
}
_index = 0;
}
...
具体就是加了几个判断, 批量处理的行数据为null的时候不进行批量操作
其实dbunit也有一个类似unitils的控制中心:DatabaseConfig类. 但是可配置的东东太少太少, 我需要的没有, 有的我一个都不需要:(, 无语.
根据唯一键清数据
这个借鉴了dbunit原来的做法(现在去掉了唯一键标识的功能), 给字段加下划线style来标识. 本来打算根据数据库的metadata信息来获取唯一键的, 但是jdbc没有提供这样的接口, 而这种用下划线标识具有更好的扩展性, 可维护性, 可移植性.
具体做法是在XlsTable的createMetaData()方法中检查字段的style, 然后利用了Column中的remark属性来存储唯一键标识信息.
Column column = null;
// 标识唯一键
byte underline = cell.getCellStyle().getFont(workbook).getUnderline();
if (underline == 1) {
column = new Column(columnName, DataType.UNKNOWN, null, null, null, "unique", null);
} else {
column = new Column(columnName, DataType.UNKNOWN);
}
columnList.add(column);
然后重新定义了一个DatabaseOperation:
public class DeleteByUniqueKeyOperation extends DatabaseOperation {
private static final Logger logger = LoggerFactory.getLogger(DeleteByUniqueKeyOperation.class);
@Override
public void execute(IDatabaseConnection connection, IDataSet dataSet) throws DatabaseUnitException, SQLException {
logger.debug("execute(connection={}, dataSet={}) - start", connection, dataSet);
IStatementFactory factory = getFactory(connection);
// for each table
ITableIterator iterator = dataSet.iterator();
while (iterator.next()) {
ITable table = iterator.getTable();
// Do not process empty table
if (isEmpty(table)) {
continue;
}
List<String> uniqueKeys = getUniqueKeys(connection, table);
if (uniqueKeys == null || uniqueKeys.size() == 0) {
continue;
}
String sql = getSql(table, connection, uniqueKeys);
executeSql(connection, factory, table, sql, uniqueKeys);
}
}
private IStatementFactory getFactory(IDatabaseConnection connection) {
DatabaseConfig databaseConfig = connection.getConfig();
IStatementFactory factory =
(IStatementFactory) databaseConfig.getProperty(DatabaseConfig.PROPERTY_STATEMENT_FACTORY);
return factory;
}
private void executeSql(IDatabaseConnection connection, IStatementFactory factory, ITable table, String sql, List<String> uniqueKeys)
throws DatabaseUnitException, SQLException {
IPreparedBatchStatement statement = null;
ITableMetaData metaData = getOperationMetaData(connection, table.getTableMetaData());
Column[] columns = metaData.getColumns();
int count = table.getRowCount();
try {
for (int i = 0; i < count; i++) {
if (statement != null) {
statement.executeBatch();
statement.clearBatch();
statement.close();
}
statement = factory.createPreparedBatchStatement(
sql, connection);
addBatchValue(table, statement, columns, uniqueKeys, i);
}
statement.executeBatch();
statement.clearBatch();
} finally {
if (statement != null) {
statement.close();
}
}
}
private void addBatchValue(ITable table, IPreparedBatchStatement statement, Column[] columns, List<String> uniqueKeys,
int row)
throws SQLException, DataSetException, TypeCastException {
for (Column column : columns) {
// 必须保证都大写
String columnName = column.getColumnName();
if (!uniqueKeys.contains(columnName)) {
continue;
}
try {
statement.addValue(table.getValue(row, columnName), column.getDataType());
} catch (TypeCastException e) {
throw new TypeCastException("Error casting value for table '"
+ table.getTableMetaData().getTableName()
+ "' and column '" + columnName + "'", e);
}
}
statement.addBatch();
}
private String getSql(ITable table, IDatabaseConnection connection, List<String> uniqueKeys) throws SQLException {
// 获取唯一键
String tableName = table.getTableMetaData().getTableName();
StringBuilder sql = new StringBuilder();
sql.append("delete ").append(tableName).append(" where ");
boolean first = true;
for (String uniqueKey : uniqueKeys) {
if (first) {
first = false;
} else {
sql.append(" and ");
}
sql.append(uniqueKey).append("=").append("? ");
}
return sql.toString();
}
private List<String> getUniqueKeys(IDatabaseConnection connection, ITable table) throws SQLException,
DataSetException {
List<String> uniqueKeys = new ArrayList<String>();
// 一种实现方式
Column[] columns = table.getTableMetaData().getColumns();
for (Column column : columns) {
if ("unique".equals(column.getRemarks())) {
uniqueKeys.add(column.getColumnName());
}
}
return uniqueKeys;
}
static boolean isEmpty(ITable table) throws DataSetException {
...
}
static ITableMetaData getOperationMetaData(IDatabaseConnection connection,
ITableMetaData metaData) throws DatabaseUnitException, SQLException {
...
}
}
dbunit中很多代码没法重用, 再一次祭出copy大法.
通过上面的步骤基本可以让unitils+dbunit能够满足目前项目的测试需要了.