Wednesday, April 11, 2012

DBUnit for integration test.

I have published a post Dive into Spring test framework which demonstrate that spring test framework will hugely improve our integration test. But must be aware that in the previous post, testing code and tested code run in same process, that says they are run in same transaction, as all integration test call javax.servlet.HttpServlet.doPost() directly.
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: