Announcement Announcement Module
Collapse
No announcement yet.
iBatis Unit Test with Lazy Loading Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • iBatis Unit Test with Lazy Loading

    I have seen a lot of good discussion around unit testing Hibernate-based DAOs when using lazy loading (for example: http://sourceforge.net/forum/forum.p...orum_id=250340). I wondered if anyone had encountered the same issues when using iBatis and has a similar solution?

    DETAILS
    ----------

    Spring 1.0.2, iBatis 2.0.5

    I have a class that extends Spring's SqlMapClientDaoSupport and has the following method:
    Code:
    public Order loadOrder(String orderId) throws DataAccessException {
            return (Order) getSqlMapClientTemplate().queryForObject("loadOrder", orderId);
    }
    The Order class includes a java.util.List property for line items. The relevant extracts from the SQL Map file are as follows:
    Code:
    <sqlMap>
      <resultMap id="orderMap" class="domain.Order">
        <result property="id" column="row_id"/>
        ...lots of other scalar fields...
        <result property="lineItems" column="row_id" select="findLineItemsByOrderId"/>
      </resultMap>
      <resultMap id="orderItemMap" class="domain.OrderLineItem">
        <result property="id" column="row_id"/>
        ...lots of other scalar fields...
      </resultMap>
    
      <select id="loadOrder" resultMap="orderMap">
        SELECT 
          row_id,
          ...other columns...
        FROM 
          order
        WHERE 
          row_id = #value#
      </select>
      <select id="findLineItemsByOrderId" parameterClass="java.lang.String" 
        resultMap="orderItemMap">
        SELECT
          row_id,
          ...other columns...
        FROM
          order_item
        WHERE
          order_id = #value#
      </select>
    </sqlMap>
    Now, this all works fine when I run the web application: I see all Order and Line Item fields. In my JUnit class, I have the following code (DbUnit stuff omitted):
    Code:
    protected DriverManagerDataSource m_dataSource; //Actually initialized in superclass
    protected SqlMapClient m_sqlMapClient; // Actually initialized in superclass
    private OrderDao m_dao;
    
    protected void setUp&#40;&#41; throws Exception &#123;
        m_dataSource = new SingleConnectionDataSource&#40;&#41;;
        m_dataSource.setDriverClassName&#40;"org.hsqldb.jdbcDriver"&#41;;
        m_dataSource.setUrl&#40;"jdbc&#58;hsqldb&#58;db-location"&#41;;
        m_dataSource.setUsername&#40;"sa"&#41;;
        m_dataSource.setPassword&#40;""&#41;;
    
        m_sqlMapClient = SqlMapClientBuilder.buildSqlMapClient&#40;...&#41;;
    
        m_dao = new SqlMapOrderDao&#40;&#41;;
        m_dao.setDataSource&#40;m_dataSource&#41;;
        m_dao.setSqlMapClient&#40;m_sqlMapClient&#41;;
    &#125;
    
    public void testLoadOrder&#40;&#41; &#123;
        Order order = m_dao.loadOrder&#40;orderId&#41;;
        System.out.println&#40;order&#41;; // Exception is thrown here!
        ...assertions omitted for brevity...
    &#125;
    When I first attempt to read the value of the line items List property, I get the following stack trace:
    java.lang.NullPointerException
    at com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelega te.endTransaction(SqlMapExecutorDelegate.java:465)
    at com.ibatis.sqlmap.engine.impl.SqlMapSessionImpl.en dTransaction(SqlMapSessionImpl.java:134)
    at com.ibatis.sqlmap.engine.impl.SqlMapClientImpl.end Transaction(SqlMapClientImpl.java:107)
    at com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelega te.autoEndTransaction(SqlMapExecutorDelegate.java: 515)
    at com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelega te.queryForList(SqlMapExecutorDelegate.java:381)
    at com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelega te.queryForList(SqlMapExecutorDelegate.java:359)
    at com.ibatis.sqlmap.engine.impl.SqlMapSessionImpl.qu eryForList(SqlMapSessionImpl.java:90)
    at com.ibatis.sqlmap.engine.impl.SqlMapClientImpl.que ryForList(SqlMapClientImpl.java:65)
    at com.ibatis.sqlmap.engine.mapping.result.loader.Res ultLoader.getResult(ResultLoader.java:65)
    at com.ibatis.sqlmap.engine.mapping.result.loader.Laz yResultLoader.loadObject(LazyResultLoader.java:94)
    at com.ibatis.sqlmap.engine.mapping.result.loader.Laz yResultLoader.invoke(LazyResultLoader.java:77)
    at $Proxy0.hashCode(Unknown Source)
    at java.util.HashMap.hash(HashMap.java:261)
    at java.util.HashMap.containsKey(HashMap.java:339)
    at java.util.HashSet.contains(HashSet.java:180)
    at org.apache.commons.lang.builder.ReflectionToString Builder.isRegistered(ReflectionToStringBuilder.jav a:141)
    at org.apache.commons.lang.builder.ToStringStyle.appe ndInternal(ToStringStyle.java:340)
    at org.apache.commons.lang.builder.ToStringStyle.appe nd(ToStringStyle.java:314)
    at org.apache.commons.lang.builder.ToStringBuilder.ap pend(ToStringBuilder.java:872)
    at domain.Order.toString(Order.java:361)
    ...etc. etc....
    If I disable lazy-loading, the unit test runs just fine.

  • #2
    In my DAO tests, I usually do programmatic transactions (as opposed to the declarative transactions used for the normal app itself), using TransactionTemplate. This is very simple, and allows all the operations to be grouped together, and have lazy loading work.

    Regards,

    Comment


    • #3
      Colin,

      Are you saying that you use TransactionTemplate in your unit test class to wrap the DAO calls? Or that you use it in your DAO methods (even the load*, find* ones) and inject the TransactionManager programatically in your unit tests (as opposed to declaratively in the as-deployed app)? I have tried both approaches, without any success. Could you please provide a snippet of sample code to illustrate your approach?

      Comment


      • #4
        In the test case itself, I just wrap the entire sequence of related calls to the DAO in a transaction template.

        Here's a sample class. Note that it's using a junit extension helper library so it only has to load the app contexts for the test once per testsuite, not on every test method. I would normally use one testcase like this for testing all daos for an entire module, but anything that works for you is ok.

        Code:
        /**
         * Test which checks that basic Mapper persistence is working.
         * 
         * Note that this test should not be considered the right way to use mapper objects in
         * a real app, since this test is concerned with persistence only. The same mapper
         * classes in real life would normally be used by service objects wrapped with
         * declarative transactions.
         * 
         * @author Colin Sampaleanu
         */
        public class BasicMappedPersistenceTest extends TestCase &#123;
        
          public static final String CONTEXT = ClassUtils.classPackageAsResourcePath&#40;BasicMappedPersistenceTest.class&#41;
              + "/BasicMappedPersistenceTestContext.xml";
        
          // ids for storing contexts as quasi singletons between individual tests. Hibernate SessionFactory
          // setup would kill us otherwise
          public static final String APP_CONTEXT_ID = "BasicMappedPersistenceTest.appContext";
          public static final String APP_CONTEXT2_ID = "BasicMappedPersistenceTest.appContext2";
        
          // --- attributes
          // per-TestSuite specific vars, via ResourceManager!
          ApplicationContext appContext;
        
          ApplicationContext appContext2;
        
          // normal vars
          // --- methods
          // --- attributes
          public static Test suite&#40;&#41; &#123;
            return new TestSetup&#40;new TestSuite&#40;BasicMappedPersistenceTest.class&#41;&#41; &#123;
        
              protected void setUp&#40;&#41; throws Exception &#123;
                try &#123;
                  ApplicationContext appContext;
                  appContext = new ClassPathXmlApplicationContext&#40;new String&#91;&#93; &#123;CONTEXT&#125;&#41;;
                  ResourceManager.addResource&#40;APP_CONTEXT_ID, appContext&#41;;
                  ApplicationContext appContext2;
                  appContext2 = new ClassPathXmlApplicationContext&#40;new String&#91;&#93; &#123;CONTEXT&#125;&#41;;
                  ResourceManager.addResource&#40;APP_CONTEXT2_ID, appContext2&#41;;
                &#125;
                catch &#40;RuntimeException t&#41; &#123;
                  // just for debugging since Eclipse swallows these
                  throw t;
                &#125;
                catch &#40;Error e&#41; &#123;
                  // just for debugging since Eclipse swallows these
                  throw e;
                &#125;
              &#125;
        
              protected void tearDown&#40;&#41; throws Exception &#123;
                ResourceManager.removeResource&#40;APP_CONTEXT_ID&#41;;
              &#125;
            &#125;;
          &#125;
        
          /*
           * &#40;non-Javadoc&#41;
           * 
           * @see junit.framework.TestCase#setUp&#40;&#41;
           */
          protected void setUp&#40;&#41; throws Exception &#123;
            appContext = &#40;ApplicationContext&#41; ResourceManager.getResource&#40;APP_CONTEXT_ID&#41;;
            appContext2 = &#40;ApplicationContext&#41; ResourceManager.getResource&#40;APP_CONTEXT2_ID&#41;;
          &#125;
        
          /**
           * A basic test to ensure object creation, saving, and cascading is working
           * Note that this test doesn't use transactions in a typical fashion, that is
           * normally, you would probably only use one transaction per method, but here
           * we actually want to force out all our changes to the db so that the db
           * would give an exception on any constraint violations. Note that short of
           * getting a whole new sesion though, the subsequent read-back is still going
           * to give us data from the session cache.
           * 
           * @throws Exception
           */
          public void testBasicCreationSaveAndCascade&#40;&#41; throws Exception &#123;
        
            final Mapper mapper = &#40;Mapper&#41; appContext.getBean&#40;"root-mapper"&#41;;
            PlatformTransactionManager tm = &#40;PlatformTransactionManager&#41; appContext
                .getBean&#40;"myTransactionManager"&#41;;
        
            TransactionTemplate transactionTemplate = new TransactionTemplate&#40;tm&#41;;
        
            final User1&#91;&#93; savedUser1 = &#123;null&#125;;
            final HashMap objMap = new HashMap&#40;&#41;;
        
            transactionTemplate.execute&#40;new TransactionCallback&#40;&#41; &#123;
        
              public Object doInTransaction&#40;TransactionStatus status&#41; &#123;
              	
              	User1 user1 = new User1&#40;null, "user1", "pass1"&#41;;
              	mapper.save&#40;user1&#41;;
              	savedUser1&#91;0&#93; = user1;
        
                return null;
              &#125;
            &#125;&#41;;
        
            // now ensure the objects can be read back ok
            final Mapper mapper2 = &#40;Mapper&#41; appContext2.getBean&#40;"root-mapper"&#41;;
            final MapperFactory mapperFactory2 = mapper2.getMapperFactory&#40;&#41;;
            PlatformTransactionManager tm2 = &#40;PlatformTransactionManager&#41; appContext2
                .getBean&#40;"myTransactionManager"&#41;;
        
            TransactionTemplate transactionTemplate2 = new TransactionTemplate&#40;tm2&#41;;
        
            transactionTemplate2.execute&#40;new TransactionCallback&#40;&#41; &#123;
        
              public Object doInTransaction&#40;TransactionStatus status&#41; &#123;
        
                // User1
              	User1 user1 = &#40;User1&#41; mapper2.load&#40;User1.class, savedUser1&#91;0&#93;.getId&#40;&#41;&#41;;
              	assertNotNull&#40;user1&#41;;
        
                return null;
              &#125;
            &#125;&#41;;
          &#125;
        &#125;
        Regards,

        Comment


        • #5
          The basic difference between Colin's unit tests and mine are that his use a Spring application context to access the DAO and TransactionManager objects, whereas (in this current project) I use standalone POJOs. Since I haven't got to the transactional part of my app yet, I have not yet defined any TransactionManager beans. Evidently, when using Spring's wrapper for the iBatis SqlMapClient class, the absence of an explicit TransactionManager in the configuration is handled gracefully and lazy loading works as expected when running the web app. However, testing the DAO in isolation is a problem.

          The solution was simply to create a test version of the sql-map-config.xml file for my DAO unit tests and add the appropriate <transactionManager> element. Now I have to decide whether or not to switch back to using Spring application contexts in my unit tests...

          Thanks, Colin, for your help.

          Comment

          Working...
          X