Friday, July 12, 2013

Dive into Spring test framework - Part2

Part1 - Dive into Spring test framework(junit3.8)

Now let's put eyes on Spring testcontext framework which is introduced since Spring 3.X.

The general Idea of Spring TextContext framework

Spring3.X has deprecated JUnit 3.8 class hierarchy, let's have a look at Spring TextContext framework. Below is a test class by means of TestContext.
  1 package com.mpos.lottery.te.draw.dao;
  2 
  3 import javax.persistence.EntityManager;
  4 import javax.persistence.PersistenceContext;
  5 
  6 import org.apache.commons.logging.Log;
  7 import org.apache.commons.logging.LogFactory;
  8 import org.junit.After;
  9 import org.junit.Before;
 10 import org.junit.Test;
 11 import org.springframework.test.annotation.Rollback;
 12 import org.springframework.test.context.ContextConfiguration;
 13 import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
 14 import org.springframework.test.context.transaction.AfterTransaction;
 15 import org.springframework.test.context.transaction.BeforeTransaction;
 16 import org.springframework.test.context.transaction.TransactionConfiguration;
 17 
 18 import com.mpos.lottery.te.common.dao.ShardKeyContextHolder;
 19 
 20 /**
 21  * Spring TestContext Framework. If extending from
 22  * <code>AbstractTransactionalJUnit4SpringContextTests</code>, you don't need to
 23  * declare <code>@RunWith</code>,
 24  * <code>TestExecutionListeners(3 default listeners)</code> and
 25  * <code>@Transactional</code>. Refer to
 26  * {@link AbstractTransactionalJUnit4SpringContextTests} for more information.
 27  * <p>
 28  * Legacy JUnit 3.8 class hierarchy is deprecated.
 29  *
 30  * @author Ramon Li
 31  */
 32 //@RunWith(SpringJUnit4ClassRunner.class)
 33 @ContextConfiguration(locations = { "/spring-service.xml", "/spring-dao.xml",
 34         "/spring-shard-datasource.xml" })
 35 @TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = false)
 36 //@TestExecutionListeners(listeners = { TransactionalTestExecutionListener.class,
 37 //        ShardAwareTestExecutionListener.class })
 38 //@Transactional
 39 public class GameDaoTest extends AbstractTransactionalJUnit4SpringContextTests {
 40         private Log logger = LogFactory.getLog(GameDaoTest.class);
 41         // Must declare @Autowire(by type) or @Resource(JSR-250)(by name)
 42         // explicitly, otherwise spring won't inject the dependency.
 43         private GameDao gameDao;
 44         @PersistenceContext(unitName = "lottery_te")
 45         private EntityManager entityManager;
 46 
 47         public GameDaoTest() {
 48                 logger.debug("GameDaoTest()");
 49                 // As spring test framework will create a auto-rollbacked transaction
 50                 // before setup a test case(even @BeforeTransaction, the data source has
 51                 // been determined), we must set the shard key before creating
 52                 // transaction, otherwise the default data source of
 53                 // <code>ShardKeyRoutingDataSource</code> will be returned if it has
 54                 // been set.
 55                 ShardKeyContextHolder.setShardKey(new Integer("2"));
 56         }
 57 
 58         @BeforeTransaction
 59         public void verifyInitialDatabaseState() {
 60                 // logic to verify the initial state before a transaction is started
 61                 logger.debug("@BeforeTransaction:verifyInitialDatabaseState()");
 62 
 63                 logger.debug("EntityManager:" + this.entityManager);
 64                 logger.debug("gameDao:" + this.gameDao);
 65         }
 66 
 67         @Before
 68         public void setUpTestDataWithinTransaction() {
 69                 // set up test data within the transaction
 70                 logger.debug("@Before:setUpTestDataWithinTransaction()");
 71         }
 72 
 73         @Test
 74         // overrides the class-level defaultRollback setting
 75         @Rollback(true)
 76         public void test_2() {
 77                 // logic which uses the test data and modifies database state
 78                 logger.debug("test_2()");
 79 
 80         }
 81 
 82         @Test
 83         public void test_1() {
 84                 logger.debug("test_1()");
 85                 // logger.debug("**** Start to query oracle data source.");
 86                 String sql = "select TYPE_NAME from GAME_TYPE where GAME_TYPE_ID=9";
 87                 // setSharkKey() won't affect here
 88                 ShardKeyContextHolder.setShardKey(new Integer("1"));
 89                 // Map<String, Object> result1 =
 90                 // this.getJdbcTemplate().queryForMap(sql);
 91 
 92                 // logger.debug("**** Start to query mysql data source.");
 93                 // setSharkKey() won't affect here
 94                 // ShardKeyContextHolder.setShardKey(new Integer("2"));
 95                 // Map<String, Object> result2 =
 96                 // this.getJdbcTemplate().queryForMap(sql);
 97 
 98                 // Avoid false positives when testing ORM code[Spring manual document]
 99                 this.entityManager.flush();
100         }
101 
102         @After
103         public void tearDownWithinTransaction() {
104                 // execute "tear down" logic within the transaction.
105                 logger.debug("@After:tearDownWithinTransaction()");
106         }
107 
108         @AfterTransaction
109         public void verifyFinalDatabaseState() {
110                 // logic to verify the final state after transaction has rolled back
111                 logger.debug("@AfterTransaction:verifyFinalDatabaseState()");
112 
113         }
114 
115 }

Be honest to say, Spring is good at its automatical transaction rollback, if you know it very well and maintain your test code with big care. The bad side is it enlarges the transaction boundary, in general the boundary of your transaction will be the invocation of a service method, spring test framework enlarges it to the test method.
It will incur below two issues:

  • Hibernate flush. If no select on given entity, hibernate won't flush DML of that entity info underlying database, until committing or flush explicitly.
  • Hibernate lazy loading. If you want to deserialize a entity out of transaction, you will know what I mean.
Below is my base test class which all transactional integration test should inherit from.
package com.mpos.lottery.te.test.integration;

import static org.junit.Assert.assertEquals;

import java.util.Calendar;
import java.util.Date;
import java.util.UUID;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
import org.springframework.test.context.transaction.AfterTransaction;
import org.springframework.test.context.transaction.BeforeTransaction;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;

import com.mpos.lottery.te.config.MLotteryContext;
import com.mpos.lottery.te.gamespec.prize.Payout;
import com.mpos.lottery.te.gamespec.sale.BaseTicket;
import com.mpos.lottery.te.hasplicense.domain.License;
import com.mpos.lottery.te.trans.domain.Transaction;

/**
 * This test will be ran against <code>DispatchServlet</code> directly, that
 * says we must support lookup <code>ApplicationContext</code> from
 * <code>ServletContext</code>, refer to
 * {@link org.springframework.web.context.support.WebApplicationContextUtils}
 * <p>
 * Spring TestContext Framework. If extending from
 * <code>AbstractTransactionalJUnit4SpringContextTests</code>, you don't need to
 * declare <code>@RunWith</code>,
 * <code>TestExecutionListeners(3 default listeners)</code> and
 * <code>@Transactional</code>. Refer to
 * {@link AbstractTransactionalJUnit4SpringContextTests} for more information.
 * <p>
 * Legacy JUnit 3.8 class hierarchy is deprecated. Under new sprint test context
 * framework, a field of property must be annotated with <code>@Autowired</code>
 * or <code>@Resource</code>(<code>@Autowired</code> in conjunction with
 * <code>@Qualifier</code>) explicitly to let spring inject dependency
 * automatically.
 * <p>
 * Reference:
 * <ul>
 * <li>https://jira.springsource.org/browse/SPR-5243</li>
 * <li>
 * http://forum.springsource.org/showthread.php?86124-How -to-register-
 * BeanPostProcessor-programaticaly</li>
 * </ul>
 * 
 * @author Ramon Li
 */

// @RunWith(SpringJUnit4ClassRunner.class)

// Refer to the doc of WebContextLoader.
@ContextConfiguration(loader = WebApplicationContextLoader.class, locations = { "spring/spring-core.xml",
        "spring/spring-core-dao.xml", "spring/game/spring-raffle.xml", "spring/game/spring-ig.xml",
        "spring/game/spring-extraball.xml", "spring/game/spring-lotto.xml", "spring/game/spring-toto.xml",
        "spring/game/spring-lfn.xml", "spring/spring-3rdparty.xml", "spring/game/spring-magic100.xml",
        "spring/game/spring-digital.xml" })
// this annotation defines the transaction manager for each test case.
@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)
// As our TEST extending from AbstractTransactionalJUnit4SpringContextTests,
// below 3 listeners have been registered by default, and it will be inherited
// by subclass.
// @TestExecutionListeners(listeners = {ShardAwareTestExecutionListener.class})
// @Transactional
public class BaseTransactionalIntegrationTest extends AbstractTransactionalJUnit4SpringContextTests {
    private static Log logger = LogFactory.getLog(BaseTransactionalIntegrationTest.class);
    // SPRING DEPENDENCIES
    /**
     * Always auto wire the data source to a javax.sql.DataSource with name
     * 'dataSource' even there are multiple data sources. It means there must be
     * a DataSource bean named 'dataSource' and a
     * <code>PlatformTransactionManager</code> named 'transactionManager'.
     * <p>
     * 
     * @see AbstractTransactionalJUnit4SpringContextTests#setDataSource(javax.sql.DataSource)
     */
    @PersistenceContext(unitName = "lottery_te")
    protected EntityManager entityManager;

    /**
     * do something if want configure test case when initialization.
     */
    public BaseTransactionalIntegrationTest() {
        // initialize MLottery context.
        MLotteryContext.getInstance();
        // enable HASP license
        this.enableLicense();
    }

    // run once for current test suite.
    @BeforeClass
    public static void beforeClass() {
        logger.trace("@BeforeClass:beforeClass()");
    }

    /**
     * logic to verify the initial state before a transaction is started.
     * <p>
     * The @BeforeTransaction methods declared in superclass will be run after
     * those of the current class. Supported by
     * {@link TransactionalTestExecutionListener}
     */
    @BeforeTransaction
    public void verifyInitialDatabaseState() throws Exception {
        logger.trace("@BeforeTransaction:verifyInitialDatabaseState()");
    }

    /**
     * Set up test data within the transaction.
     * <p>
     * The @Before methods of superclass will be run before those of the current
     * class. No other ordering is defined.
     * <p>
     * NOTE: Any before methods (for example, methods annotated with JUnit 4's
     * <code>@Before</code>) and any after methods (such as methods annotated
     * with JUnit 4's <code>@After</code>) are executed within a transaction.
     */
    @Before
    public void setUpTestDataWithinTransaction() {
        logger.trace("@Before:setUpTestDataWithinTransaction()");
        this.initializeMLotteryContext();
    }

    /**
     * execute "tear down" logic within the transaction.
     * <p>
     * The @After methods declared in superclass will be run after those of the
     * current class.
     */
    @After
    public void tearDownWithinTransaction() {
        logger.trace("@After:tearDownWithinTransaction()");
    }

    /**
     * logic to verify the final state after transaction has rolled back.
     * <p>
     * The @AfterTransaction methods declared in superclass will be run after
     * those of the current class.
     */
    @AfterTransaction
    public void verifyFinalDatabaseState() {
        logger.trace("@AfterTransaction:verifyFinalDatabaseState()");
    }

    @AfterClass
    public static void afterClass() {
        logger.trace("@AfterClass:afterClass()");
    }

    // ----------------------------------------------------------------
    // HELPER METHODS
    // ----------------------------------------------------------------

    protected void initializeMLotteryContext() {
        logger.debug("Retrieve a ApplicationContext(" + this.applicationContext + ").");
        MLotteryContext.getInstance().setBeanFactory(this.applicationContext);
    }

    protected void printMethod() {
        StringBuffer lineBuffer = new StringBuffer("+");
        for (int i = 0; i < 120; i++) {
            lineBuffer.append("-");
        }
        lineBuffer.append("+");
        String line = lineBuffer.toString();

        // Get the test method. If index=0, it means get current method.
        StackTraceElement eles[] = new Exception().getStackTrace();
        // StackTraceElement eles[] = new Exception().getStackTrace();
        // for (StackTraceElement ele : eles){
        // System.out.println("class:" + ele.getClassName());
        // System.out.println("method:" + ele.getMethodName());
        // }
        String className = eles[1].getClassName();
        int index = className.lastIndexOf(".");
        className = className.substring((index == -1 ? 0 : (index + 1)));

        String method = className + "." + eles[1].getMethodName();
        StringBuffer padding = new StringBuffer();
        for (int i = 0; i < line.length(); i++) {
            padding.append(" ");
        }
        logger.info(line);
        String methodSig = (method + padding.toString()).substring(0, line.length() - 3);
        logger.info("| " + methodSig + "|");
        logger.info(line);
    }

    protected void enableLicense() {
        Calendar cal = Calendar.getInstance();
        cal.setTime(new Date());
        cal.set(Calendar.YEAR, cal.get(Calendar.YEAR) + 1);
        License.getInstance().setExpireDate(cal);
    }

    protected String uuid() {
        UUID uuid = UUID.randomUUID();
        String uuidStr = uuid.toString();
        return uuidStr.replace("-", "");
    }

    // ----------------------------------------------------------------
    // ASSERTION METHODS
    // ----------------------------------------------------------------

    protected void assertTransaction(Transaction expectedTrans, Transaction actualTrans) {
        assertEquals(expectedTrans.getId(), actualTrans.getId());
        assertEquals(expectedTrans.getGameId(), actualTrans.getGameId());
        assertEquals(expectedTrans.getTotalAmount().doubleValue(),
                actualTrans.getTotalAmount().doubleValue(), 0);
        assertEquals(expectedTrans.getTicketSerialNo(), actualTrans.getTicketSerialNo());
        assertEquals(expectedTrans.getDeviceId(), actualTrans.getDeviceId());
        assertEquals(expectedTrans.getMerchantId(), actualTrans.getMerchantId());
        assertEquals(expectedTrans.getType(), actualTrans.getType());
        assertEquals(expectedTrans.getOperatorId(), actualTrans.getOperatorId());
        assertEquals(expectedTrans.getTraceMessageId(), actualTrans.getTraceMessageId());
        assertEquals(expectedTrans.getResponseCode(), actualTrans.getResponseCode());
    }

    protected void assertTicket(BaseTicket expectTicket, BaseTicket actualTicket) {
        assertEquals(expectTicket.getSerialNo(), actualTicket.getSerialNo());
        assertEquals(expectTicket.getStatus(), actualTicket.getStatus());
        assertEquals(expectTicket.getTotalAmount().doubleValue(),
                actualTicket.getTotalAmount().doubleValue(), 0);
        assertEquals(expectTicket.getMultipleDraws(), actualTicket.getMultipleDraws());
        assertEquals(expectTicket.getMobile(), actualTicket.getMobile());
        assertEquals(expectTicket.getCreditCardSN(), actualTicket.getCreditCardSN());
        assertEquals(expectTicket.getDevId(), actualTicket.getDevId());
        assertEquals(expectTicket.getMerchantId(), actualTicket.getMerchantId());
        assertEquals(expectTicket.getOperatorId(), actualTicket.getOperatorId());
        assertEquals(expectTicket.getTicketFrom(), actualTicket.getTicketFrom());
        assertEquals(expectTicket.getTicketType(), actualTicket.getTicketType());
        assertEquals(expectTicket.getTransType(), actualTicket.getTransType());
        assertEquals(expectTicket.isCountInPool(), actualTicket.isCountInPool());
        assertEquals(expectTicket.getGameInstance().getId(), actualTicket.getGameInstance().getId());
        assertEquals(expectTicket.getPIN(), actualTicket.getPIN());
    }

    protected void assertPayout(Payout exp, Payout actual) {
        assertEquals(exp.getTransaction().getId(), actual.getTransaction().getId());
        assertEquals(exp.getGameId(), actual.getGameId());
        assertEquals(exp.getGameInstanceId(), actual.getGameInstanceId());
        assertEquals(exp.getDevId(), actual.getDevId());
        assertEquals(exp.getMerchantId(), actual.getMerchantId());
        assertEquals(exp.getOperatorId(), actual.getOperatorId());
        assertEquals(exp.getTicketSerialNo(), actual.getTicketSerialNo());
        assertEquals(exp.getBeforeTaxObjectAmount().doubleValue(), actual.getBeforeTaxObjectAmount()
                .doubleValue(), 0);
        assertEquals(exp.getBeforeTaxTotalAmount().doubleValue(), actual.getBeforeTaxTotalAmount()
                .doubleValue(), 0);
        assertEquals(exp.getTotalAmount().doubleValue(), actual.getTotalAmount().doubleValue(), 0);
        assertEquals(exp.getNumberOfObject(), actual.getNumberOfObject());
    }

    // ----------------------------------------------------------------
    // SPRINT DEPENDENCIES INJECTION
    // ----------------------------------------------------------------

    public EntityManager getEntityManager() {
        return entityManager;
    }

    public void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }
}

What happen if one single test method make 2 separated requests?

In my project, there is a services named 'sell' for client to make a sale, and a corresponding service named 'enquiry' to query that sale.

Now we plan to test the service 'enquiry' and write a test case named 'testEnquiry'. ok, how do we prepare the test data of a sale which will be quired? There are at least 2 options.

Prepare test data and import them into database before running test

By this mean, there are possibilities that your prepared test data doesn't meet the specification of service 'sell'. That says you prepared test data may write a column named 'gameId', however 'sell' service won't write that column. In such case, your test case will pass, however in production environment, the 'enquiry' service will fail.

Call 'sell' service in 'testEnquiry' method

The pseud code seem as below:
public static void testEnquiry(){
    callSellService();
    callCancelService();
    //assert ouput
}
The callSellService() and callEnquiryService() are in same single transaction. Here I will give a real case in my project. The callSellService() will generates tickets(List), and callEnquiryService() will query tickets generated by sale service, then marshell it into xml.
What makes me surprise is that the tickets entities retrieved by callEnquiryService() are same with tickets entities generated by callSellService(). I mean they are same java object, not only the same fields/properties.
However in production, may fields in tickets retrieved by callEnquiryService() are missed, as in production environemnt callSellService() and callEnquiryService() are completely different 2 transactions.

Which option is better? Or 3rd option?


I prefer the 2nd option, prepare test data by real transactions. Then how to face its problem? after some research, the solution is simple and effect.
public static void testEnquiry(){
    callSellService();
    this.entityManager.flush();
    this.entityManager.clear();
    callCancelService();
    //assert ouput
}
  • this.entityManager.flush() will flush all entity state to underlying database. This must be  called, otherwise all change of entity will be lost.
  • this.entityManager.clear() will clear all entities, and make them in detached state, then any subsequent call to entity manager will new entity.

May DBUnit is another choice, however that means I have to convert my sql script into xml, oh, that is a big challenge.