Je pense qu'il existe encore de nombreux cas (2020) d'utilisation de DBUnit comme outil de test unitaire dans des projets de développement Java + RDBMS. DBUnit est un outil très pratique et efficace pour économiser du travail de développement car il peut utiliser le format xls et le format CSV pour les données initiales de DB et les données d'assertion après exécution, mais il est un peu difficile à utiliser en raison du problème du format de données Excel et des spécifications de POI. Il y a un point.
Plus précisément, comme mentionné dans le sujet, il y a un problème qu'il n'est pas possible de distinguer entre une chaîne de caractères vide de type chaîne de caractères variable (type VARCHAR) et NULL, et que la valeur du type de date (DATETIME, TIMESTAMP, DATE, HEURE, etc.) ne peut pas être définie avec précision. Il y a.
Cet article décrit comment résoudre ce problème. La version de DbUnit dans cet article est 2.5.4.
Dans DBUnit, la méthode getValue (int row, String column)
de la classe XlsTable
obtient la valeur de la cellule Excel et la convertit en la valeur définie dans la base de données.
XlsTable#Extrait de getValue
int type = cell.getCellType();
switch (type)
{
case HSSFCell.CELL_TYPE_NUMERIC:
if (HSSFDateUtil.isCellDateFormatted(cell))
{
return cell.getDateCellValue();
}
return new BigDecimal(cell.getNumericCellValue());
case HSSFCell.CELL_TYPE_STRING:
return cell.getStringCellValue();
case HSSFCell.CELL_TYPE_FORMULA:
throw new DataTypeException("Formula not supported at row=" +
row + ", column=" + column);
case HSSFCell.CELL_TYPE_BLANK:
return null;
case HSSFCell.CELL_TYPE_BOOLEAN:
return cell.getBooleanCellValue() ? Boolean.TRUE : Boolean.FALSE;
case HSSFCell.CELL_TYPE_ERROR:
throw new DataTypeException("Error at row=" + row +
", column=" + column);
default:
throw new DataTypeException("Unsupported type at row=" + row +
", column=" + column);
}
Comme vous pouvez le voir dans le code ci-dessus, si le type de données de la cellule est numérique et dans un format particulier, il s'agit du type de date. En outre, si la cellule est vide, elle est définie sur NULL. Par conséquent, la différence entre les valeurs de type de date définies dans le DB s'affiche en fonction de l'exactitude des données de date dans Excel. Par exemple, sur Excel, ce que vous mettez dans «2020/1/24 10: 00» peut devenir «2020/1/24 10: 00: 01» dans la colonne de type DATETIME de DB. De plus, si la valeur de la cellule est vide, elle sera uniformément nulle, il n'est donc pas possible de créer des données de chaîne de caractères vides.
DBUnit fournit également une fonction pour sortir les données lues à partir de la base de données (mais sans s'y limiter) dans un fichier Excel.
La sortie vers Excel est effectuée par la méthode write (IDataSet dataSet, OutputStream out)
de la classe XlsDataSet
.
La valeur à définir dans la cellule est acquise dans la méthode write
.
XlsDataSet#Extrait d'écriture
// write table data
for (int j = 0; j < table.getRowCount(); j++)
{
HSSFRow row = sheet.createRow(j + 1);
for (int k = 0; k < columns.length; k++)
{
Column column = columns[k];
Object value = table.getValue(j, column.getColumnName());
if (value != null)
{
HSSFCell cell = row.createCell((short)k);
cell.setEncoding(HSSFCell.ENCODING_UTF_16);
cell.setCellValue(DataType.asString(value));
}
}
}
Le code ci-dessus récupère la valeur stockée dans ʻITable dans ʻIDataSet
et la définit dans la cellule.
Si la valeur est «null», la cellule n'est pas générée, donc ce sera une cellule vide sur l'affichage Excel.
À partir du code source DBUnit vu ci-dessus, vous pouvez également voir qu'en plus du contenu de traitement DBUnit lui-même, l'utilisateur ne peut pas modifier la méthode de conversion de la valeur de la cellule Excel pour le type de données.
Par conséquent, afin d'utiliser DBUnit commodément, je vais modifier le code source de XlsTable
et XlsDataSet
pour créer ma propre classe et utiliser DBUnit de cette classe.
Dans la méthode XlsTable # getValue
, modifiez le type de données de la cellule pour définir le type de date s'il correspond au modèle de type de date, même s'il s'agit d'un type chaîne.
De plus, si vous écrivez «null» sur la cellule, définissez une valeur NULL dans la base de données.
XlsTable#Réparer getValue
int type = cell.getCellType();
switch (type)
{
case HSSFCell.CELL_TYPE_NUMERIC:
if (HSSFDateUtil.isCellDateFormatted(cell))
{
return cell.getDateCellValue();
}
return new BigDecimal(cell.getNumericCellValue());
case HSSFCell.CELL_TYPE_STRING:
/*Mise en œuvre originale (à partir d'ici)*/
String cellValue = cell.getRichStringCellValue().getString();
//La valeur de la cellule est"null"Défini sur NULL pour
if ("null".equals(cellValue)) { return null; }
//La valeur de la cellule est"yyyy/MM/dd HH:mm:ss"Dans le cas du format, défini en analysant le type de date
if (Pattern.compile("\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2}").matcher(cellValue).matches()) {
return new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(cellValue);
}
//Autre que ce qui précède, définissez la valeur de la cellule telle quelle
return cellValue;
/*Mise en œuvre originale (jusqu'à ici)*/
case HSSFCell.CELL_TYPE_FORMULA:
throw new DataTypeException("Formula not supported at row=" +
row + ", column=" + column);
case HSSFCell.CELL_TYPE_BLANK:
//Si la cellule est vide, renvoie une chaîne vide
return "";
case HSSFCell.CELL_TYPE_BOOLEAN:
return cell.getBooleanCellValue() ? Boolean.TRUE : Boolean.FALSE;
case HSSFCell.CELL_TYPE_ERROR:
throw new DataTypeException("Error at row=" + row +
", column=" + column);
default:
throw new DataTypeException("Unsupported type at row=" + row +
", column=" + column);
}
Si vous souhaitez uniquement comparer les paramètres de données initiaux des données JUnit et DB, il vous suffit de corriger la lecture des informations de cellule, mais vous pouvez également utiliser la sortie au format xls des données DB pour rationaliser la création des données de test. Par conséquent, le traitement de sortie des informations de cellule est également corrigé en fonction de la correction de la lecture des informations de cellule.
XlsDataSet#Correction d'écriture
// write table data
for (int j = 0; j < table.getRowCount(); j++)
{
HSSFRow row = sheet.createRow(j + 1);
for (int k = 0; k < columns.length; k++)
{
Column column = columns[k];
Object value = table.getValue(j, column.getColumnName());
/*Mise en œuvre originale (à partir d'ici)*/
if (null == value) {
cell.setCellValue("null");
} else if (value instanceof java.sql.Timestamp) {
cell.setCellValue(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(value));
} else {
cell.setCellValue(DataType.asString(value));
}
/*Mise en œuvre originale (jusqu'à ici)*/
}
}
Si la valeur est nulle, définissez la valeur de la cellule sur la chaîne «null».
Si le type de données est de type Timestamp, définissez une chaîne de caractères au format aaaa / MM / jj HH: mm: ss
.
Avec la politique ci-dessus, nous empruntons en fait le code source de XlsDataSet
et XlsTable
pour définir notre propre classe.
Étant donné que la partie modifiée n'utilise que la bibliothèque JDK, je pense qu'elle peut être compilée si l'environnement peut utiliser DBUnit.
Définissez la classe «MyXlsDataSet» comme une version modifiée de «XlsDataSet». La classe XlsTable
est définie comme une classe privée de package et n'est pas accessible à partir de packages externes.
Ici, nous définissons la classe MyXlsTable
comme une classe interne de MyXlsDataSet
.
En ce qui concerne le type de date, considérez le type DATE et le type TIME en plus du type DATETIME, définissez un HashMap
avec la clé DateFormat
comme valeur, et définissez le Motif
de chaque clé de Map
. S'il est inspecté et correspond, il est analysé par la valeur «DateFormat» associée à la clé. Cela peut être une «Liste» ou un tableau qui stocke une combinaison de «Pattern» et «DateFormat» au lieu de «Map».
Veuillez noter que ce code est uniquement à des fins de test et ne prend pas en compte la synchronisation.
Si vous souhaitez utiliser cette classe pour lire un fichier Excel à partir de plusieurs threads, vous devez déterminer s'il faut créer SimpleDateFormat
à chaque fois ou le stocker dans ThreadLocal
.
MyXlsDataSet
public class MyXlsDataSet extends AbstractDataSet {
private static final Logger logger = LoggerFactory.getLogger(MyXlsDataSet.class);
/*Mise en œuvre originale (à partir d'ici)*/
private static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd");
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss");
/*Mise en œuvre originale (jusqu'à ici)*/
private final ITable[] _tables;
/**
* Creates a new XlsDataSet object that loads the specified Excel document.
*/
public DateFormattedXlsDataSet(File file) throws IOException, DataSetException {
this(new FileInputStream(file));
}
/**
* Creates a new XlsDataSet object that loads the specified Excel document.
*/
public DateFormattedXlsDataSet(InputStream in) throws IOException, DataSetException {
HSSFWorkbook workbook = new HSSFWorkbook(in);
_tables = new ITable[workbook.getNumberOfSheets()];
for (int i = 0; i < _tables.length; i++) {
_tables[i] = new DateFormattedXlsTable(workbook.getSheetName(i), workbook.getSheetAt(i));
}
}
/**
* Write the specified dataset to the specified Excel document.
*/
public static void write(IDataSet dataSet, OutputStream out)
throws IOException, DataSetException {
logger.debug("write(dataSet=" + dataSet + ", out=" + out + ") - start");
HSSFWorkbook workbook = new HSSFWorkbook();
int index = 0;
ITableIterator iterator = dataSet.iterator();
while (iterator.next()) {
// create the table i.e. sheet
ITable table = iterator.getTable();
ITableMetaData metaData = table.getTableMetaData();
HSSFSheet sheet = workbook.createSheet(metaData.getTableName());
// write table metadata i.e. first row in sheet
// workbook.setSheetName(index, metaData.getTableName(), HSSFWorkbook.ENCODING_UTF_16);
workbook.setSheetName(index, metaData.getTableName());
HSSFRow headerRow = sheet.createRow(0);
Column[] columns = metaData.getColumns();
for (int j = 0; j < columns.length; j++) {
Column column = columns[j];
HSSFCell cell = headerRow.createCell((short) j);
// cell.setEncoding(HSSFCell.ENCODING_UTF_16);
cell.setCellValue(column.getColumnName());
}
// write table data
for (int j = 0; j < table.getRowCount(); j++) {
HSSFRow row = sheet.createRow(j + 1);
for (int k = 0; k < columns.length; k++) {
Column column = columns[k];
Object value = table.getValue(j, column.getColumnName());
/*Mise en œuvre originale (à partir d'ici)*/
HSSFCell cell = row.createCell((short) k);
if (null == value) {
cell.setCellValue("null");
} else if (value instanceof java.sql.Timestamp) {
cell.setCellValue(TIMESTAMP_FORMAT.format(value));
} else if (value instanceof java.sql.Date) {
cell.setCellValue(DATE_FORMAT.format(value));
} else if (value instanceof Time) {
cell.setCellValue(TIME_FORMAT.format(value));
} else {
cell.setCellValue(DataType.asString(value));
}
/*Mise en œuvre originale (jusqu'à ici)*/
// if (value != null) {
// HSSFCell cell = row.createCell((short) k);
// cell.setEncoding(HSSFCell.ENCODING_UTF_16);
// cell.setCellValue(DataType.asString(value));
// }
}
}
index++;
}
// write xls document
workbook.write(out);
out.flush();
}
////////////////////////////////////////////////////////////////////////////
// AbstractDataSet class
protected ITableIterator createIterator(boolean reversed) throws DataSetException {
// logger.debug("createIterator(reversed=" + reversed + ") - start");
return new DefaultTableIterator(_tables, reversed);
}
private static class MyXlsTable extends AbstractTable {
/*Mise en œuvre originale (à partir d'ici)*/
//Puisqu'il s'agit de UT, la synchronisation n'est pas prise en compte
private static final HashMap<Pattern, SimpleDateFormat> DATETIME_PATTERN_MAP =
new HashMap<Pattern, SimpleDateFormat>();
static {
DATETIME_PATTERN_MAP.put(
Pattern.compile("\\d{4}/\\d{2}/\\d{2}"), new SimpleDateFormat("yyyy/MM/dd"));
DATETIME_PATTERN_MAP.put(
Pattern.compile("\\d{4}-\\d{2}-\\d{2}"), new SimpleDateFormat("yyyy-MM-dd"));
DATETIME_PATTERN_MAP.put(
Pattern.compile("\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2}"),
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"));
DATETIME_PATTERN_MAP.put(
Pattern.compile("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}"),
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
/*Mise en œuvre originale (jusqu'à ici)*/
// private static final Logger logger = LoggerFactory.getLogger(XlsTable.class);
private final ITableMetaData _metaData;
private final Sheet _sheet;
private final DecimalFormatSymbols symbols = new DecimalFormatSymbols();
public DateFormattedXlsTable(String sheetName, Sheet sheet) throws DataSetException {
int rowCount = sheet.getLastRowNum();
if (rowCount >= 0 && sheet.getRow(0) != null) {
_metaData = createMetaData(sheetName, sheet.getRow(0));
} else {
_metaData = new DefaultTableMetaData(sheetName, new Column[0]);
}
_sheet = sheet;
// Needed for later "BigDecimal"/"Number" conversion
symbols.setDecimalSeparator('.');
}
static ITableMetaData createMetaData(String tableName, Row sampleRow) {
logger.debug("createMetaData(tableName={}, sampleRow={}) - start", tableName, sampleRow);
List columnList = new ArrayList();
for (int i = 0; ; i++) {
Cell cell = sampleRow.getCell(i);
if (cell == null) {
break;
}
String columnName = cell.getRichStringCellValue().getString();
if (columnName != null) {
columnName = columnName.trim();
}
// Bugfix for issue ID 2818981 - if a cell has a formatting but no name also ignore it
if (columnName.length() <= 0) {
// logger.debug("The column name of column # {} is empty - will skip here assuming the
// last column was reached", String.valueOf(i));
break;
}
Column column = new Column(columnName, DataType.UNKNOWN);
columnList.add(column);
}
Column[] columns = (Column[]) columnList.toArray(new Column[0]);
return new DefaultTableMetaData(tableName, columns);
}
////////////////////////////////////////////////////////////////////////////
// ITable interface
public int getRowCount() {
logger.debug("getRowCount() - start");
return _sheet.getLastRowNum();
}
public ITableMetaData getTableMetaData() {
logger.debug("getTableMetaData() - start");
return _metaData;
}
public Object getValue(int row, String column) throws DataSetException {
if (logger.isDebugEnabled()) {
logger.debug("getValue(row={}, columnName={}) - start", Integer.toString(row), column);
}
assertValidRowIndex(row);
int columnIndex = getColumnIndex(column);
Cell cell = _sheet.getRow(row + 1).getCell(columnIndex);
if (cell == null) {
return null;
}
int type = cell.getCellType();
switch (type) {
case Cell.CELL_TYPE_NUMERIC:
CellStyle style = cell.getCellStyle();
if (DateUtil.isCellDateFormatted(cell)) {
return getDateValue(cell);
} else if (XlsDataSetWriter.DATE_FORMAT_AS_NUMBER_DBUNIT.equals(
style.getDataFormatString())) {
// The special dbunit date format
return getDateValueFromJavaNumber(cell);
} else {
return getNumericValue(cell);
}
case Cell.CELL_TYPE_STRING:
/*Mise en œuvre originale (à partir d'ici)*/
String cellValue = cell.getRichStringCellValue().getString();
if ("null".equals(cellValue)) {
return null;
}
Set<Pattern> patternSet = DATETIME_PATTERN_MAP.keySet();
for (Pattern pattern : patternSet) {
if (pattern.matcher(cellValue).matches()) {
SimpleDateFormat format = DATETIME_PATTERN_MAP.get(pattern);
try {
return format.parse(cellValue);
} catch (ParseException e) {
continue;
}
}
}
return cellValue;
/*Mise en œuvre originale (jusqu'à ici)*/
case Cell.CELL_TYPE_FORMULA:
throw new DataTypeException("Formula not supported at row=" + row + ", column=" + column);
case Cell.CELL_TYPE_BLANK:
return ""; //Mise en œuvre originale
case Cell.CELL_TYPE_BOOLEAN:
return cell.getBooleanCellValue() ? Boolean.TRUE : Boolean.FALSE;
case Cell.CELL_TYPE_ERROR:
throw new DataTypeException("Error at row=" + row + ", column=" + column);
default:
throw new DataTypeException("Unsupported type at row=" + row + ", column=" + column);
}
}
protected Object getDateValueFromJavaNumber(Cell cell) {
logger.debug("getDateValueFromJavaNumber(cell={}) - start", cell);
double numericValue = cell.getNumericCellValue();
BigDecimal numericValueBd = new BigDecimal(String.valueOf(numericValue));
numericValueBd = stripTrailingZeros(numericValueBd);
return new Long(numericValueBd.longValue());
}
protected Object getDateValue(Cell cell) {
logger.debug("getDateValue(cell={}) - start", cell);
double numericValue = cell.getNumericCellValue();
Date date = DateUtil.getJavaDate(numericValue);
return new Long(date.getTime());
}
/**
* Removes all trailing zeros from the end of the given BigDecimal value up to the decimal
* point.
*
* @param value The value to be stripped
* @return The value without trailing zeros
*/
private BigDecimal stripTrailingZeros(BigDecimal value) {
if (value.scale() <= 0) {
return value;
}
String valueAsString = String.valueOf(value);
int idx = valueAsString.indexOf(".");
if (idx == -1) {
return value;
}
for (int i = valueAsString.length() - 1; i > idx; i--) {
if (valueAsString.charAt(i) == '0') {
valueAsString = valueAsString.substring(0, i);
} else if (valueAsString.charAt(i) == '.') {
valueAsString = valueAsString.substring(0, i);
// Stop when decimal point is reached
break;
} else {
break;
}
}
BigDecimal result = new BigDecimal(valueAsString);
return result;
}
protected BigDecimal getNumericValue(Cell cell) {
logger.debug("getNumericValue(cell={}) - start", cell);
String formatString = cell.getCellStyle().getDataFormatString();
String resultString = null;
double cellValue = cell.getNumericCellValue();
if ((formatString != null)) {
if (!formatString.equals("General") && !formatString.equals("@")) {
logger.debug("formatString={}", formatString);
DecimalFormat nf = new DecimalFormat(formatString, symbols);
resultString = nf.format(cellValue);
}
}
BigDecimal result;
if (resultString != null) {
try {
result = new BigDecimal(resultString);
} catch (NumberFormatException e) {
logger.debug("Exception occurred while trying create a BigDecimal. value={}",
resultString);
// Probably was not a BigDecimal format retrieved from the excel. Some
// date formats are not yet recognized by HSSF as DateFormats so that
// we could get here.
result = toBigDecimal(cellValue);
}
} else {
result = toBigDecimal(cellValue);
}
return result;
}
/**
* @param cellValue
* @return
* @since 2.4.6
*/
private BigDecimal toBigDecimal(double cellValue) {
String resultString = String.valueOf(cellValue);
// To ensure that intergral numbers do not have decimal point and trailing zero
// (to restore backward compatibility and provide a string representation consistent with
// Excel)
if (resultString.endsWith(".0")) {
resultString = resultString.substring(0, resultString.length() - 2);
}
BigDecimal result = new BigDecimal(resultString);
return result;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getName()).append("[");
sb.append("_metaData=").append(this._metaData == null ? "null" : this._metaData.toString());
sb.append(", _sheet=").append(this._sheet == null ? "null" : "" + this._sheet);
sb.append(", symbols=").append(this.symbols == null ? "null" : "" + this.symbols);
sb.append("]");
return sb.toString();
}
}
}
Le code source cité était le dernier 2.6.0 au 24 janvier 2020, mais comme la version de DBUnit utilisée pour la vérification est 2.5.4, j'ai commenté les paramètres liés à l'encodage Excel. .. Si vous souhaitez utiliser la version 2.6.0 ou supérieure, veuillez décommenter la ligne suivante.
// workbook.setSheetName(index, metaData.getTableName(), HSSFWorkbook.ENCODING_UTF_16);
// cell.setEncoding(HSSFCell.ENCODING_UTF_16);
Si la classe propriétaire ci-dessus est compilée avec succès, l'utilisation est exactement la même que celle de XlsDataSet
.
Cependant, comme DBUnit n'autorise pas la sortie de cellules vides par défaut, il est préférable d'ajouter un processus pour définir DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS
sur true pour la DatabaseConnection
acquise.
Définir les données Excel dans DB
DatabaseConnection connection = new DatabaseConnection(sqlConnection, schemaName);
connection.getConfig().setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, true);
IDataSet xlsDataSet = new MyXlsDataset(new File(xlsFilePath));
DatabaseOperation.CLEAN_INSERT.execute(connection, compositDataSet);
Comparez les données Excel et la base de données
DatabaseConnection connection = new DatabaseConnection(sqlConnection, schemaName);
connection.getConfig().setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, true);
IDataSet expected= new MyXlsDataset(new File(expectedXlsFilePath));
QueryDataSet actual = new QueryDataSet(connection);
Assert.assertEquals(expected, actual);
Sortie des données de base de données dans un fichier Excel
DatabaseConnection connection = new DatabaseConnection(sqlConnection, schemaName);
connection.getConfig().setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, true);
QueryDataSet dbDataSet= new QueryDataSet(connection);
FileOutputStream fileOutputStream = new FileOutputStream(outputFilePath);
MyXlsDataset.write(dbDataSet, fileOutputStream);
Pour les projets qui utilisent gradle, vous souhaiterez peut-être utiliser DBUnit comme tâche gradle pour exporter ou importer des bases de données. Bien sûr, cela est possible, mais dans ce cas, la classe MyXlsDataSet
ci-dessus devrait être une bibliothèque jar.
DBUnit dispose d'une licence LGPL. Veuillez noter que si vous souhaitez inclure le code ci-dessus dans le code d'implémentation, par exemple lors de la distribution du code de test du programme créé, vous devez vous conformer à la licence LGPL.