Now we face a new situation, imagine that how do maintain data consistency when our testing code call tested code remotely, for example by http, or TCP socket etc. In this case, testing code and tested code run in seperated process, spring can't manage both client and server side transactions. How do we automatic such integration test scenario?
The main challenges in such remote integration test are how to make data consistent for each test case. Only definite input can produce definite output, then how to maintain the underlying database at a definite state for each test case? DBUnit is born for that.
Before running a test case, we can clean and insert a given initial test data set, this will guarantee that we will run our test case against given test data, then we can expect a given output, and finally compare expected result against underlying database.
Code will explain everything. Below is a base test class from which all test class should extend.
1 package net.mpos.igpe.test; 2 3 import java.io.File; 4 import java.io.FileOutputStream; 5 import java.io.IOException; 6 import java.sql.Connection; 7 import java.sql.Driver; 8 import java.sql.DriverManager; 9 import java.sql.ResultSet; 10 import java.sql.SQLException; 11 import java.sql.Statement; 12 import java.text.SimpleDateFormat; 13 import java.util.Date; 14 import java.util.HashMap; 15 import java.util.LinkedList; 16 import java.util.List; 17 import java.util.Map; 18 19 import net.mpos.igpe.common.tlvutilities.TLVElement; 20 import net.mpos.igpe.common.tlvutilities.TLVParser; 21 import net.mpos.igpe.core.Constants; 22 import net.mpos.igpe.util.SecurityMeasurements; 23 24 import org.apache.commons.logging.Log; 25 import org.apache.commons.logging.LogFactory; 26 import org.dbunit.Assertion; 27 import org.dbunit.DatabaseUnitException; 28 import org.dbunit.database.DatabaseConnection; 29 import org.dbunit.database.IDatabaseConnection; 30 import org.dbunit.database.QueryDataSet; 31 import org.dbunit.dataset.DataSetException; 32 import org.dbunit.dataset.IDataSet; 33 import org.dbunit.dataset.ITable; 34 import org.dbunit.dataset.SortedTable; 35 import org.dbunit.dataset.filter.DefaultColumnFilter; 36 import org.dbunit.dataset.xml.FlatXmlDataSet; 37 import org.dbunit.ext.oracle.Oracle10DataTypeFactory; 38 import org.dbunit.operation.DatabaseOperation; 39 import org.dbunit.util.fileloader.FlatXmlDataFileLoader; 40 import org.junit.After; 41 import org.junit.Before; 42 43 public class BaseAcceptanceTest { 44 protected Log logger = LogFactory.getLog(BaseAcceptanceTest.class); 45 public IDatabaseConnection dbConn; 46 // NOTE: DATA_KEY and MAC_KEY must be same with table 'operator_session'. 47 // Before runnint test, you must reload the testing data into db in order to 48 // set the operator_session.create_time as current time. 49 public String dataKey = "BS1ZbvLkmOyESBpyZ0XqoiZH8WkYsL2g"; 50 public String macKey = "7yuxr9fYh/2lmtv5YnybIQTm+jdAr58V+ifRZskMfO8="; 51 public String igpeHost = "192.168.2.107"; 52 public int igpePort = 3000; 53 // public String igpeHost = "192.168.2.136"; 54 // public int igpePort = 8899; 55 public String opLoginName = "OPERATOR-LOGIN"; 56 public String batchNo = "200901"; 57 public long deviceId = 111; 58 59 /** 60 * Load test data for each test case automatically. As all IGPE integration 61 * tests are TE backed, those master test data(oracle_masterdata.sql) which 62 * used to support IGPE/TE launching must be imported manually. 63 */ 64 @Before 65 public void setUp() throws Exception { 66 // we can reuse IDatabaseConnection, it represents a specific underlying 67 // database connection. 68 // must use this constructor which specify 'scheme', otherwise a 69 // 'AmbiguousTableNameException' will be thrown out. 70 dbConn = new DatabaseConnection(setupConnection(), "ramonal", true); 71 /** 72 * Refer to DBUnit FAQ. 73 * <p> 74 * Why am I getting an "The configured data type factory 'class 75 * org.dbunit.dataset.datatype.DefaultDataTypeFactory' might cause 76 * problems with the current database ..." ? 77 * <p> 78 * This warning occurs when no data type factory has been configured and 79 * DbUnit defaults to its 80 * org.dbunit.dataset.datatype.DefaultDataTypeFactory which supports a 81 * limited set of RDBMS. 82 */ 83 dbConn.getConfig().setProperty("http://www.dbunit.org/properties/datatypeFactory", 84 new Oracle10DataTypeFactory()); 85 86 // initialize database 87 FlatXmlDataFileLoader loader = new FlatXmlDataFileLoader(); 88 /** 89 * DbUnit uses the first tag for a table to define the columns to be 90 * populated. If the following records for this table contain extra 91 * columns, these ones will therefore not be populated. 92 * <p> 93 * To solve this, either define all the columns of the table in the 94 * first row (using NULL values for the empty columns), or use a DTD to 95 * define the metadata of the tables you use. 96 */ 97 Map replaceMap = new HashMap(); 98 replaceMap.put("[NULL]", null); 99 replaceMap.put("[SYS_TIMESTAMP]", 100 new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); 101 loader.addReplacementObjects(replaceMap); 102 // lookup data file from classpath 103 IDataSet testData = loader.load("/testdata.xml"); 104 DatabaseOperation.CLEAN_INSERT.execute(dbConn, testData); 105 logger.info("Load test data successfully"); 106 } 107 108 @After 109 public void tearDown() throws Exception { 110 // release database connection 111 if (dbConn != null) 112 dbConn.close(); 113 } 114 115 protected void assertTable(List<DbAssertTable> actualAssertTables) 116 throws SQLException, DataSetException, DatabaseUnitException, IOException { 117 this.assertTable(null, actualAssertTables); 118 } 119 120 /** 121 * Assert data set. Load expected data set file from classpath, this file 122 * must be located at the same package with the test class and has a 123 * convenient name which follows 124 * '{TestClassName}.{TestMethodName}.expected.xml', for example 125 * 'PayoutAcceptanceTest.testPayout_WithoutLGPrize_OldPOS_OK.expected.xml' 126 */ 127 protected void assertTable(Map replacementMap, List<DbAssertTable> actualAssertTables) 128 throws SQLException, DataSetException, DatabaseUnitException, IOException { 129 String className = this.getClass().getCanonicalName(); 130 className = className.replace(".", "/"); 131 this.assertTable(replacementMap, actualAssertTables, PATH_MODE_CLASSPATH + "/" + className 132 + "." + getCurrentMethodName() + ".expected.xml"); 133 } 134 135 protected String getCurrentMethodName() { 136 StackTraceElement e[] = Thread.currentThread().getStackTrace(); 137 for (StackTraceElement s : e) { 138 // only test case method name can start with 'testXXX'. 139 if (s.getMethodName().startsWith("test")) 140 return s.getMethodName(); 141 } 142 return null; 143 } 144 145 /** 146 * Assert data set in <code>expectedDataSetFile</code> against underlying 147 * database. Only data set defined in <code>expectedDataSetFile</code> will 148 * be compared. 149 * <p> 150 * You can specify <code>expectedDataSetFile</code> in two styles: 151 * <ul> 152 * <li>lookup data set file from file system, it must start with "file://", 153 * for example, "file://e:/tmp/expected.xml"</li> 154 * <li>lookup data set file from classpath, it must start with 155 * "classpath://", for example, 156 * "classpath:///net/mpos/igpe/transactions/payout/Payout.testPayout_WithLGPrize_NewPOS_OK.xml" 157 * </li> 158 * </ul> 159 * If <code>expectedDataSetFile</code> doesn't start with either "file:" or 160 * "classpath://", default "classpath://" will be assumed. 161 * 162 * @param actualAssertTables The database table definitions which used to 163 * limit the returned rows and also may apply ordering. 164 * @param expectedDataSetFile The file of expected data set. 165 * @param replacementMap A replacement map used to replace placholder in 166 * expected data set file. 167 */ 168 protected void assertTable(Map replacementMap, List<DbAssertTable> actualAssertTables, 169 String expectedDataSetFile) throws SQLException, DataSetException, 170 DatabaseUnitException, IOException { 171 // only compare all tables defined in expectedDataSetFile 172 IDataSet expectedDataSet = this.loadDataSet(expectedDataSetFile, replacementMap); 173 String[] expectedTableNames = expectedDataSet.getTableNames(); 174 for (String expectedTableName : expectedTableNames) { 175 if (logger.isDebugEnabled()) 176 logger.debug("Start to compare expected table - " + expectedTableName); 177 ITable actualTable = null; 178 ITable expectedTable = expectedDataSet.getTable(expectedTableName); 179 DbAssertTable dbTableDef = this.lookupDbAssertTable(actualAssertTables, 180 expectedTableName); 181 if (dbTableDef == null) { 182 // match all rows in underlying database table. 183 actualTable = dbConn.createTable(expectedTableName); 184 } else { 185 if (dbTableDef.getQuery() != null) 186 actualTable = dbConn.createQueryTable(expectedTableName, dbTableDef.getQuery()); 187 if (dbTableDef.getSort() != null) { 188 // By default, database table snapshot taken by DbUnit are 189 // sorted by 190 // primary keys. If a table does not have a primary key or 191 // the primary 192 // key is automatically generated by your database, the rows 193 // ordering is 194 // not predictable and assertEquals will fail. 195 actualTable = new SortedTable(actualTable, dbTableDef.getSort()); 196 // must be invoked immediately after the constructor 197 ((SortedTable) actualTable).setUseComparable(true); 198 expectedTable = new SortedTable(expectedTable, dbTableDef.getSort()); 199 // must be invoked immediately after the constructor 200 ((SortedTable) expectedTable).setUseComparable(true); 201 } 202 } 203 // Ignoring some columns in comparison, only compare columns defined 204 // in expected table. 205 actualTable = DefaultColumnFilter.includedColumnsTable(actualTable, expectedTable 206 .getTableMetaData().getColumns()); 207 208 // assert 209 Assertion.assertEquals(expectedTable, actualTable); 210 } 211 } 212 213 private DbAssertTable lookupDbAssertTable(List<DbAssertTable> tables, String tableName) { 214 for (DbAssertTable dbTable : tables) { 215 if (tableName.equalsIgnoreCase(dbTable.getTableName())) 216 return dbTable; 217 } 218 logger.info("No DbAssertTable found by tableName=" + tableName); 219 return null; 220 } 221 222 protected static final String PATH_MODE_FILE = "file://"; 223 protected static final String PATH_MODE_CLASSPATH = "classpath://"; 224 225 protected IDataSet loadDataSet(String datasetFile, Map replacementMap) throws IOException, 226 DataSetException { 227 FlatXmlDataFileLoader loader = new FlatXmlDataFileLoader(); 228 if (replacementMap != null) 229 loader.addReplacementObjects(replacementMap); 230 IDataSet dataset = null; 231 if (datasetFile.startsWith(PATH_MODE_FILE)) { 232 dataset = loader.getBuilder().build( 233 new File(datasetFile.substring(PATH_MODE_FILE.length()))); 234 } else if (datasetFile.startsWith(PATH_MODE_CLASSPATH)) { 235 dataset = loader.load(datasetFile.substring(PATH_MODE_CLASSPATH.length())); 236 } 237 return dataset; 238 } 239 240 /** 241 * Represents the underlying database table which will used to be compared 242 * with expected tables. 243 * 244 * @author Ramon Li 245 */ 246 protected class DbAssertTable { 247 private String tableName; 248 private String query; 249 private String[] sort; 250 251 public DbAssertTable(String tableName, String query, String[] sort) { 252 if (tableName == null) 253 throw new IllegalArgumentException("argument 'tableName' can't be null"); 254 this.tableName = tableName; 255 this.query = query; 256 this.sort = sort; 257 } 258 259 /** 260 * The name of asserted table, actually it should be the SQL result name 261 * of <code>query</code>, and the name must match with a table defined 262 * in expected test data file. 263 */ 264 public String getTableName() { 265 return tableName; 266 } 267 268 /** 269 * A SQl query used to retrieve specific number of rows from underlying 270 * database. If you want to get some rows which satify specific 271 * condition of a given table, a query should be specified. 272 * <p> 273 * Also you may want to retrieve a result by join different real 274 * database tables, in this case, the <code>tableName</code> doesn't 275 * need to be a real database table name, but it must match with the 276 * corresponding table defined in expeced data set file. 277 * <p> 278 * If query is null, the <code>tableName</code> must be a real database 279 * table name, and all rows in that table will be returned. 280 */ 281 public String getQuery() { 282 return query; 283 } 284 285 /** 286 * As DBUnit doesn't guarantee the order of returned rows, we would 287 * better specify the columns used to sort rows explicitly. 288 * <p> 289 * If sort columns are null, no sort operation will be performed. 290 */ 291 public String[] getSort() { 292 return sort; 293 } 294 } 295 296 /** 297 * Run a given SQL against underlying database. 298 */ 299 protected void sqlExec(IDatabaseConnection conn, String sql) throws Exception { 300 Statement state = conn.getConnection().createStatement(); 301 state.execute(sql); 302 state.close(); 303 conn.getConnection().commit(); 304 } 305 306 /** 307 * Run a given SQL against underlying database, and return the int value of 308 * sql result. 309 */ 310 protected int sqlQueryInt(IDatabaseConnection conn, String sql) throws Exception { 311 int result = 0; 312 Statement state = conn.getConnection().createStatement(); 313 ResultSet rs = state.executeQuery(sql); 314 if (rs.next()) { 315 result = rs.getInt(1); 316 } 317 rs.close(); 318 state.close(); 319 conn.getConnection().commit(); 320 return result; 321 } 322 323 public IgpeClient setupIgpeClient() throws Exception { 324 Connection conn = null; 325 try { 326 // retrieve latest data/mac key 327 conn = setupConnection(); 328 return new IntegrationTestIgpeClient(igpeHost, igpePort); 329 } finally { 330 if (conn != null) 331 conn.close(); 332 } 333 } 334 335 /** 336 * Generate timestamp string of current time. 337 */ 338 protected String generateTimestamp() { 339 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); 340 return sdf.format(new Date()); 341 } 342 343 /** 344 * Decrypt the message body into plain text. 345 * 346 * @param respTlvs The collection of responded TLVs. 347 * @return the plain text of message body which is encrypted. 348 * @throws Exception when encounter any exceptions. 349 */ 350 protected String getPlainMessageBody(LinkedList<TLVElement> respTlvs) throws Exception { 351 return this.getPlainMessageBody(respTlvs, this.dataKey); 352 } 353 354 protected String getPlainMessageBody(LinkedList<TLVElement> respTlvs, String key) 355 throws Exception { 356 TLVElement msgBodyTlv = TLVParser.GetObjectFromList(respTlvs, Constants.TAG_MESSAGE_BODY); 357 return SecurityMeasurements.TripleDESCBCDecryptToString(msgBodyTlv.GetValueAsString(), key); 358 } 359 360 public static Connection setupConnection(String url, Driver driver, String userName, 361 String passwd) throws SQLException { 362 DriverManager.registerDriver(driver); 363 return DriverManager.getConnection(url, userName, passwd); 364 } 365 366 public static Connection setupConnection() throws SQLException { 367 return setupConnection("jdbc:oracle:thin:@192.168.2.9:1521/orcl", 368 new oracle.jdbc.driver.OracleDriver(), "ramonal", "ramonal"); 369 } 370 371 private final static int MODE_LOAD = 1; 372 private final static int MODE_EXPORT = 2; 373 374 /** 375 * How to extract a flat XML dataset from my database? 376 */ 377 public static void main(String args[]) throws Exception { 378 if (args.length != 2) { 379 System.out.println("[USAGE]"); 380 System.out.println("java " + BaseAcceptanceTest.class.getCanonicalName() 381 + " -l [test data source file to be loaded]"); 382 System.out.println("OR"); 383 System.out.println("java " + BaseAcceptanceTest.class.getCanonicalName() 384 + " -e [test data destination file to be exported]"); 385 System.exit(0); 386 } 387 388 int mode = -1; 389 if ("-l".equals(args[0])) 390 mode = MODE_LOAD; 391 else if ("-e".equals(args[0])) 392 mode = MODE_EXPORT; 393 else 394 throw new IllegalArgumentException("unsupport mode:" + args[0]); 395 String testDataFile = args[1]; 396 397 // we can reuse IDatabaseConnection, it represents a specific underlying 398 // database connection. 399 // must use this constructor which specify 'scheme', otherwise a 400 // 'AmbiguousTableNameException' will be thrown out. 401 IDatabaseConnection connection = new DatabaseConnection(setupConnection(), "ramonal", true); 402 connection.getConfig().setProperty("http://www.dbunit.org/properties/datatypeFactory", 403 new Oracle10DataTypeFactory()); 404 405 if (MODE_LOAD == mode) { 406 FlatXmlDataFileLoader loader = new FlatXmlDataFileLoader(); 407 Map replaceMap = new HashMap(); 408 replaceMap.put("[NULL]", null); 409 loader.addReplacementObjects(replaceMap); 410 IDataSet testData = loader.getBuilder().build(new File(testDataFile)); 411 DatabaseOperation.CLEAN_INSERT.execute(connection, testData); 412 System.out.println("Load test data(" + testDataFile + ") successfully"); 413 return; 414 } 415 416 if (MODE_EXPORT == mode) { 417 // partial database export 418 QueryDataSet partialDataSet = new QueryDataSet(connection); 419 partialDataSet.addTable("TE_SEQUENCE"); 420 partialDataSet.addTable("SYS_CONFIGURATION"); 421 partialDataSet.addTable("TELCO"); 422 partialDataSet.addTable("GPE"); 423 partialDataSet.addTable("MERCHANT"); 424 partialDataSet.addTable("TELCO_MERCHANT"); 425 partialDataSet.addTable("DEVICE_PHYSICAL_AVAILABILITY"); 426 partialDataSet.addTable("DEVICE_TYPE"); 427 partialDataSet.addTable("DEVICES"); 428 partialDataSet.addTable("HMAC_KEY"); 429 partialDataSet.addTable("DEPARTMENT"); 430 partialDataSet.addTable("ROLE"); 431 partialDataSet.addTable("LANGUAGES"); 432 partialDataSet.addTable("OPERATOR"); 433 partialDataSet.addTable("OPERATOR_MERCHANT"); 434 partialDataSet.addTable("LOTTO_FUN_TYPE"); 435 partialDataSet.addTable("LOTTO_OPERATION_PARAMETERS"); 436 partialDataSet.addTable("IG_OPERATION_PARAMETERS"); 437 partialDataSet.addTable("WINNER_TAX_POLICY"); 438 partialDataSet.addTable("TAX_DATE_RANGE"); 439 partialDataSet.addTable("WINNER_TAX_THRESHOLDS"); 440 partialDataSet.addTable("GAME_TYPE"); 441 partialDataSet.addTable("GAME"); 442 partialDataSet.addTable("GAME_MERCHANT"); 443 partialDataSet.addTable("GAME_INSTANCE"); 444 partialDataSet.addTable("GAME_RESULTS"); 445 partialDataSet.addTable("TE_TRANSACTION"); 446 partialDataSet.addTable("TE_TRANSACTION_MSG"); 447 partialDataSet.addTable("TE_TICKET"); 448 partialDataSet.addTable("TE_LOTTO_ENTRY"); 449 partialDataSet.addTable("WINNING"); 450 partialDataSet.addTable("WINNING_STATISTICS"); 451 partialDataSet.addTable("PRIZE_PARAMETERS"); 452 partialDataSet.addTable("PAYOUT_DETAIL"); 453 partialDataSet.addTable("PAYOUT"); 454 partialDataSet.addTable("MERCHANT_GAME_PROPERTIES"); 455 partialDataSet.addTable("IG_GAME_INSTANCE"); 456 partialDataSet.addTable("INSTANT_TICKET"); 457 partialDataSet.addTable("INSTANT_TICKET_VIRN"); 458 partialDataSet.addTable("OPERATOR_SESSION"); 459 partialDataSet.addTable("ACCESS_RIGHT"); 460 partialDataSet.addTable("ROLE_ACCESS"); 461 partialDataSet.addTable("BD_LOGO"); 462 partialDataSet.addTable("BD_MARKETING_MESSAGE"); 463 partialDataSet.addTable("WINNING_OBJECT"); 464 partialDataSet.addTable("BD_PRIZE_LOGIC"); 465 partialDataSet.addTable("BD_PRIZE_OBJECT"); 466 partialDataSet.addTable("BD_PRIZE_LEVEL"); 467 partialDataSet.addTable("BD_PRIZE_LEVEL_ITEM"); 468 partialDataSet.addTable("BD_PRIZE_GROUP"); 469 partialDataSet.addTable("BD_PRIZE_GROUP_ITEM"); 470 partialDataSet.addTable("OBJECT_PRIZE_PARAMETERS"); 471 partialDataSet.addTable("DW_OPERATOR"); 472 partialDataSet.addTable("DW_CARD"); 473 partialDataSet.addTable("DW_MERCHANT_TOPUP_LOG"); 474 partialDataSet.addTable("WINNING_DAILY_CASH"); 475 partialDataSet.addTable("PRIZE_LOGIC"); 476 FlatXmlDataSet.write(partialDataSet, new FileOutputStream(testDataFile)); 477 System.out.println("export test data(" + testDataFile + ") successfully!"); 478 return; 479 } 480 } 481 }In this base test class, setUp() will load all initial test data from "testdata.xml" which locate at root package.
Below is a test class extending from BaseAcceptanceTest.
1 package net.mpos.igpe.transactions.payout; 2 3 import static org.junit.Assert.assertEquals; 4 5 import java.util.Arrays; 6 import java.util.HashMap; 7 import java.util.LinkedList; 8 import java.util.Map; 9 10 import net.mpos.igpe.common.tlvutilities.TLVElement; 11 import net.mpos.igpe.common.tlvutilities.TLVParser; 12 import net.mpos.igpe.core.Constants; 13 import net.mpos.igpe.test.BaseAcceptanceTest; 14 import net.mpos.igpe.test.IgpeClient; 15 16 import org.junit.Test; 17 18 public class PayoutAcceptanceTest extends BaseAcceptanceTest { 19 20 @Test 21 public void testPayout_WithoutLGPrize_OldPOS_OK() throws Exception { 22 IgpeClient igpeClient = this.setupIgpeClient(); 23 String traceMsgId = generateTimestamp(); 24 LinkedList<TLVElement> resp = igpeClient 25 .igpe("1.4", traceMsgId, generateTimestamp(), opLoginName, 26 Constants.REQ_PAYOUT + "", deviceId + "", batchNo, 27 Constants.INVALIDATION_VALUE + "", "#S-123456#PIN-111#2#", dataKey, macKey); 28 String respCode = TLVParser.GetObjectFromList(resp, Constants.TAG_RESPONSE_CODE) 29 .GetValueAsString(); 30 assertEquals("#1#200##", respCode); 31 String msgBody = this.getPlainMessageBody(resp); 32 System.out.println(msgBody); 33 34 // ----- assert database 35 Map replacementMap = new HashMap(); 36 // dynamicaly replace palceholder in expected test data file. 37 replacementMap.put("${TICKET_SERIALNO}", "Dwl8yOheqKjhNA2RNW9GFQ=="); 38 replacementMap.put("${TRACE_MSG_ID}", traceMsgId); 39 String transId = TLVParser.GetObjectFromList(resp, Constants.TAG_TRANSACTION_ID) 40 .GetValueAsString(); 41 this.assertTable(replacementMap, Arrays.asList(new DbAssertTable("TE_TICKET", 42 "select * from TE_TICKET where SERIAL_NO='Dwl8yOheqKjhNA2RNW9GFQ=='", 43 new String[] { "GAME_INSTANCE_ID" }), 44 // only one expected row, no need to set sorting columns. 45 new DbAssertTable("TE_Transaction", "select * from TE_TRANSACTION where ID='" 46 + transId + "'", null))); 47 } 48 49 }In the test case testPayout_WithoutLGPrize_OldPOS_OK(), it will call assertTable() which will compare expected data set against the underlying database. You can check assertTable() for detail implementation, it is easy to understand. Our expected test data file: PayoutAcceptanceTest.testPayout_WithLGPrize_NewPOS_OK.expected.xml
1 <?xml version='1.0' encoding='UTF-8'?> 2 <dataset> 3 <TE_TICKET ID="TICKET-111" GAME_INSTANCE_ID="GII-111" TRANSACTION_ID="TRANS-111" VERSION="1" SERIAL_NO="Dwl8yOheqKjhNA2RNW9GFQ==" TOTAL_AMOUNT="2500.1" IS_WINNING="0" STATUS="5" PIN="f5e09f731f7dffc2a603a7b9b977b2ca" IS_OFFLINE="0" IS_COUNT_IN_POOL="1" IS_BLOCK_PAYOUT="0" SETTLEMENT_FLAG="0" OPERATOR_ID="OPERATOR-111" DEV_ID="111" MERCHANT_ID="111" EXTEND_TEXT="90091b1caee72b14c5269c9214e66dab" TICKET_TYPE="1"/> 4 <TE_TICKET ID="TICKET-112" GAME_INSTANCE_ID="GII-112" TRANSACTION_ID="TRANS-111" VERSION="1" SERIAL_NO="Dwl8yOheqKjhNA2RNW9GFQ==" TOTAL_AMOUNT="2500.1" IS_WINNING="0" STATUS="5" PIN="f5e09f731f7dffc2a603a7b9b977b2ca" IS_OFFLINE="0" IS_COUNT_IN_POOL="1" IS_BLOCK_PAYOUT="0" SETTLEMENT_FLAG="0" OPERATOR_ID="OPERATOR-111" DEV_ID="111" MERCHANT_ID="111" EXTEND_TEXT="90091b1caee72b14c5269c9214e66dab" TICKET_TYPE="1"/> 5 <TE_TICKET ID="TICKET-113" GAME_INSTANCE_ID="GII-113" TRANSACTION_ID="TRANS-111" VERSION="1" SERIAL_NO="Dwl8yOheqKjhNA2RNW9GFQ==" TOTAL_AMOUNT="2500.1" IS_WINNING="0" STATUS="1" PIN="f5e09f731f7dffc2a603a7b9b977b2ca" IS_OFFLINE="0" IS_COUNT_IN_POOL="1" IS_BLOCK_PAYOUT="0" SETTLEMENT_FLAG="0" OPERATOR_ID="OPERATOR-111" DEV_ID="111" MERCHANT_ID="111" EXTEND_TEXT="90091b1caee72b14c5269c9214e66dab" TICKET_TYPE="1"/> 6 <TE_TRANSACTION OPERATOR_ID="OPERATOR-111" GPE_ID="GPE-111" DEV_ID="111" MERCHANT_ID="111" VERSION="0" TYPE="302" TRACE_MESSAGE_ID="${TRACE_MSG_ID}" RESPONSE_CODE="200" TICKET_SERIAL_NO="${TICKET_SERIALNO}" BATCH_NO="200901" SETTLEMENT_FLAG="0" GAME_ID="GAME-111"/> 7 </dataset>There is another open source project Unitils which can make using DBunit easier, but it depends on too many third party projects which also depended by my own project, to avoid version conflict of same library, I refuse it. Actually once we have implemented BaseAcceptanceTest, the overhead of writing test code have been significantly decreased.
No comments:
Post a Comment