发布新日志

  • jboss4自带的spring-2.0.7, 千万注意呀--1周悬而未决的问题终于解决啦

    2012-08-02 13:33:12

    使用Webx+velocity+ibatis, service用jboss,编译成功,但启动jboss总是报错,server.log相关出错代码如下:
     ERROR o.s.web.context.ContextLoader - Context initialization failed
    org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Failed to import bean definitions from relative location [common/webx-component-and-root.xml]
    Offending resource: ServletContext resource [/WEB-INF/webx.xml]; nested exception is org.springframework.beans.factory.BeanDefinitionStoreException: Unexpected exception parsing XML document from ServletContext resource [/WEB-INF/common/webx-component-and-root.xml]; nested exception is java.lang.NoSuchMethodError: org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseBeanDefinitionAttributes(Lorg/w3c/dom/Element;Ljava/lang/String;Lorg/springframework/beans/factory/config/BeanDefinition;Lorg/springframework/beans/factory/support/AbstractBeanDefinition;)Lorg/springframework/beans/factory/support/AbstractBeanDefinition;
     at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:68) ~[tmp1631473704819645958spring-2.0.7.jar:2.0.7]
     at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:85) ~[tmp1631473704819645958spring-2.0.7.jar:2.0.7]
     at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:76) ~[tmp1631473704819645958spring-2.0.7.jar:2.0.7]
     at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.importBeanDefinitionResource(DefaultBeanDefinitionDocumentReader.java:195) ~[tmp1631473704819645958spring-2.0.7.jar:2.0.7]
     
    起初一起怀疑xml文件有问题,修改xml->编译->打包,启动jboss,重复N次,总是同样的问题, 后来看到spring-2.0.7.jar:2.0.7,猜是不是spring.jar问题,试着把spring-2.5.6.jar复制过来,重启jboss,搞定。
     
    问题的具体原因待查~~~~
  • Spring+iBatis+DBUnit 进行单元测试[转发]

    2011-03-29 15:39:46

    Spring+iBatis+DBUnit测试详解
    在很多成功的软件项目中,测试自动化往往是关键的层面。DBUnit允许开发人员在测试之前给目标数据库植入测试数据,在测试完毕后,再将数据库恢复到测试前的状态。在最近的一个项目中,我尝试使用用DBUnit对Spring+iBatis的架构进行测试,下面记录了DBUnit的使用过程和遇到的一些问题。

    测试环境
    首先,我们建立一个测试环境(基于Maven 2和Oracle数据库*)。数据表名Account。

    数据库
    先建立一个测试数据表(数据库为Oracle*)

    Account.sql
    CREATE TABLE Account
    ("ID" NUMBER,
      "USERNAME" VARCHAR2(256 BYTE) NOT NULL ENABLE,
      "PASSWORD" VARCHAR2(256 BYTE),
        CONSTRAINT "ACCOUNT_UK_ID" UNIQUE ("ID"),
        CONSTRAINT "ACCOUNT_PK" PRIMARY KEY ("USERNAME")
    )

    这里我暂时不想涉及Sequence,所以主键**是username,而不是ID,并且ID允许为NULL。这是因为Sequence的递增是不可恢复的,如果项目对记录ID是否连续不是特别在意的话,可以在自己的项目中建立,只要稍微修改一下iBatis配置文件中的SQL语句就可以了。这里我们先屏蔽这个问题。

    * DBUnit测试Oracle数据库时,帐户最好不要拥有DBA权限,否则会出现org.dbunit.database.AmbiguousTableNameException: COUNTRIES 错误。如果帐户必须具备DBA权限,那么就需要在执行new DatabaseConnection时,明确给定SCHEMA(名称必须大写),详细说明参考下文多处代码注释和“org.dbunit.database.AmbiguousTableNameException异常”章节。

    ** 表必须存在主键,否则返回org.dbunit.dataset.NoPrimaryKeyException错误。

    Spring配置文件
    ApplicationContext.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
       "http://www.springframework.org/dtd/spring-beans.dtd">
      
    <beans>
       <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
           <property name="locations">
               <list>
                   <value>classpath:database.properties</value>
               </list>
           </property>
       </bean>
       <bean id="dataSource"
          class="org.springframework.jdbc.datasource.DriverManagerDataSource">
          <property name="driverClassName" value="${database.connection.driver_class}"/>
          <property name="url" value="${database.connection.url}"/>
          <property name="username" value="${database.connection.username}"/>
          <property name="password" value="${database.connection.password}"/>
       </bean>
       <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
          <property name="configLocation">
              <value>SqlMapConfig.xml</value>
          </property>
          <property name="dataSource" ref="dataSource"/>
       </bean>
      
       <bean id="accountManager" class="com.wang.dbunit.AccountManager">
          <property name="sqlMapClient" ref="sqlMapClient"/>
       </bean>
    </beans>

    database.properties
    database.connection.driver_class=oracle.jdbc.driver.OracleDriver
    database.connection.url=jdbc:oracle:thin:@xxx.xxx.xxx.xxx:1521:test
    database.connection.username=username
    database.connection.password=password

    iBatis配置文件
    SqlMapConfig.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE sqlMapConfig
       PUBLIC"-//iBATIS.com//DTD SQL Map Config 2.0//EN"
       "http://www.ibatis.com/dtd/sql-map-config-2.dtd">
    <sqlMapConfig>
       <settings
          useStatementNamespaces="false"
          cacheModelsEnabled="true"
          enhancementEnabled="true"
          lazyLoadingEnabled="true"
          maxRequests="32"
          maxSessions="10"
          maxTransactions="5"
       />
       <sqlMap resource="Account.xml"/>
    </sqlMapConfig>

    Account.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPEsqlMap
       PUBLIC"-//iBATIS.com//DTD SQL Map 2.0//EN"
       "http://www.ibatis.com/dtd/sql-map-2.dtd">
    <sqlMap namespace="Account">
       <resultMap id="accountMap" class="com.wang.dbunit.Account">
          <result property="id" column="id" jdbcType="NUMBER" nullValue="0"/>
          <result property="userName" column="username" jdbcType="VARCHAR2"/>
           <result property="password" column="password" jdbcType="VARCHAR2"/>
       </resultMap>
       <!--** preserve ************************************** -->
       <sql id="id-select">
          <![CDATA[
           SELECT id_sequence.nextval AS id FROM dual
          ]]>
       </sql>
       <!--*************************************************** -->
       <sql id="account-select">
          <![CDATA[
           SELECTid, username
          ]]>
          <dynamic prepend=",">
              <isEqual
               property="includePassword"
               compareValue="true">
                password
              </isEqual>
          </dynamic>
          FROMaccount
       </sql>
       <sql id="account-where">
          <![CDATA[
           username=#userName:VARCHAR2#
          ]]>
          <dynamic>
              <isNotNull
               property="password"
               prepend="AND ">
                 <![CDATA[
                  password=#password:VARCHAR2#
                 ]]>
              </isNotNull>
          </dynamic>
       </sql>
       <select id="getAccount"
        parameterClass="com.wang.dbunit.Account"
        resultMap="accountMap">
          <include refid="account-select"/>
          <dynamic prepend=" WHERE">
              <isNotNull
               property="userName">
                 <include refid="account-where"/>
              </isNotNull>
          </dynamic>
       </select>
       <!--**************************************************** -->
       <sql id="account-insert">
          <![CDATA[
           INSERT INTO account(username, password
          ]]>
          <dynamic prepend=",">
              <isNotEqual
               property="id"
               compareValue="0">
              <![CDATA[
                id
             ]]>
              </isNotEqual>
          </dynamic>
          )
       </sql>
      
       <sql id="account-insert-values">
          <![CDATA[
           VALUES(#userName:VARCHAR2#, #password:VARCHAR2#
          ]]>
          <dynamic prepend=",">
              <isNotEqual
               property="id"
               compareValue="0">
              <![CDATA[
                #id:NUMBER#
             ]]>
              </isNotEqual>
          </dynamic>
          )
       </sql>
       <insert id="createAccount"
        parameterClass="com.wang.dbunit.Account">
          <isEqual
           property="generateIdFromSequence"
           compareValue="true">
              <include refid="id-select"/>
          </isEqual>
          <include refid="account-insert"/>
          <include refid="account-insert-values"/>
       </insert>
    </sqlMap>

    (这个配置文件中预留了未来使用 sequence 的可能)

    DBUnit配置文件
    我们通过一个xml种子文件(seedfile)为DBUnit提供测试数据,文件中的数据会被DBUnit在测试开始前自动植入数据表,这个文件结构很简单:

    dataSet.xml
    <?xml version='1.0' encoding='UTF-8'?>
    <DATASET>
       <ACCOUNT id='1'
          username='Drew'
          password='Smith'/>
       <ACCOUNT id='2'
          username='Nick'
          password='Marquiss'/>
       <ACCOUNT id='3'
          username='Jose'
          password='Whitson'/>
    </DATASET>

    “ACCOUNT”就是表名称,它的属性就是字段内容。

    代码
    辅助类Accout.java
    package com.wang.dbunit;
    public class Account
    {
       private boolean generateIdFromSequence=false;
       private boolean includePassword = false;
       private long id = 0;
       private String userName = null;
       private String password = null;
       public boolean getGenerateIdFromSequence()
       {
          return generateIdFromSequence;
       }
       public void setGenerateIdFromSequence(boolean generateIdFromSequence)
       {
          this.generateIdFromSequence =generateIdFromSequence;
       }
       public void setId(long id)
       {
          this.id =id;
       }
       public long getId()
       {
          return this.id;
       }
       public String getPassword()
       {
          return password;
       }
       public void setPassword(String password)
       {
          this.password =password;
       }
       public String getUserName()
       {
          return userName;
       }
       public void setUserName(String userName)
       {
          this.userName =userName;
       }
       public boolean isIncludePassword()
       {
          return includePassword;
       }
       public void setIncludePassword(boolean includePassword)
       {
          this.includePassword =includePassword;
       }
    }

    业务类AccountManager.java
    package com.wang.dbunit;
    import com.ibatis.sqlmap.client.SqlMapClient;
    public class AccountManager
    {
       protected SqlMapClient sqlMap = null;
       public void setSqlMapClient(SqlMapClient sqlMapClient)
       {
          this.sqlMap =sqlMapClient;
       }
       public Account getAccount(String userName, String password, boolean includePassword) throws Exception
       {
          Account account = new Account();
          account.setUserName(userName);
          account.setPassword(password);
          account.setIncludePassword(includePassword);
          Account ret
             = (Account)(sqlMap.queryForObject("getAccount", account));
          return ret;
       }
       public void createAccount(String userName, String password) throws Exception
       {
          Account account = new Account();
          account.setUserName(userName);
          account.setPassword(password);
          sqlMap.insert("createAccount",account);
       }
    }

    好了,我们完成了了全部测试环境,接下来我们要开始编写测试用例。

    测试
    DatabaseTestCase类
    DBUnit提供了一个抽象类: DatabaseTestCase,它继承自 JUnit的 TestCase,这个类有两个方法需要重载:

    protecte abstract IDatabaseConnection getConnection() throws Exception;
    protected abstract IDataSet getDataSet() throws Exception;

    getConnection用于告诉测试用例如何找到数据库连接;getDataSet用于告诉测试用例如何获取测试数据文件(dataSet.xml)。下面我们先继承这个抽象类编写测试用例:

    package com.wang.dbunit;
    import java.io.FileInputStream;
    import java.sql.Connection;
    import java.util.Properties;
    import javax.sql.DataSource;
    import com.wang.dbunit.Account;
    import org.apache.log4j.Logger;
    import org.apache.log4j.LogManager;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import org.dbunit.DatabaseTestCase;
    import org.dbunit.database.DatabaseConnection;
    import org.dbunit.database.IDatabaseConnection;
    import org.dbunit.dataset.IDataSet;
    import org.dbunit.dataset.xml.FlatXmlDataSet;
    import org.dbunit.operation.DatabaseOperation;
    public class HelloDBUnit extends DatabaseTestCase
    {
       static Logger logger
          = LogManager.getLogger(HelloDBUnit.class.getName());
       privatestatic ApplicationContext context;
       protected Properties props = new Properties();
       public HelloDBUnit() throws IOException
       {
          super();
          props.load(Resources.getResourceAsStream(
             "database.properties"));
          context = newClassPathXmlApplicationContext(
             "classpath:ApplicationContext.xml");
       }
       ////////////////////////////////////////////////
       @Override
       protected IDatabaseConnection getConnection() throws Exception
       {
          DataSourcedataSource
          = (DataSource)context.getBean("dataSource");
          Connectionconnection = dataSource.getConnection();
          // 如果所用测试帐户是 DBA,为了避免出现 AmbiguousTableNameException
          // 异常,下面必须改写为 newDatabaseConnection(connection, SCHEMA)
          // 形式。注意SCHEMA 要大写**
          return new DatabaseConnection(connection);
       }
       @Override
       protected IDataSet getDataSet()throws Exception
       {
          return new FlatXmlDataSet(
                   new FileInputStream("bin/dataSet.xml"));
       }
       ///////////////////////////////////////////////
       @Override
       protected DatabaseOperation getSetUpOperation() throws Exception
       {
          return DatabaseOperation.REFRESH;
       }
       @Override
       protected DatabaseOperation getTearDownOperation() throws Exception
       {
          return DatabaseOperation.NONE;
       }
       ////////////////////////////////////////////////////
       public void testSelectAccount()
       {
          AccountManager manager
             = (AccountManager)context.getBean("accountManager");
          try
          {
             Accountaccount
                = manager.getAccount("Nick", "Marquiss", true);
             assertNotNull(account);
          }
          catch (Exceptione)
          {
             logger.error(e.getMessage(),e);
          }
       }
       public void testCreateAccount()
       {
          AccountManager manager
             = (AccountManager)context.getBean("accountManager");
          try
          {
              manager.createAccount("TEST", "test");
          }
          catch(Exception e)
          {
              logger.error(e.getMessage(),e);
          }
       }
    }

    在getConnection方法中,我们通过Spring配置文件获得数据连接。

    除了前面那两个方法外,我们还重载了 getSetUpOperation 和 getTearDownOperation 方法:DatabaseOperation.REFRESH 告诉DBUnit在测试开始前比较数据库和配置文件,如果发现测试数据不存或不一致在则插入或更新***。DatabaseOperation.NONE表示什么也不做。

    这个CASE应该可以运行的很好,但是如果我们把 getTearDownOperation改成:

    @Override
    protected DatabaseOperation getTearDownOperation() throws Exception
    {
       return DatabaseOperation.DELETE_ALL;
    }

    就会发生java.sql.SQLException: Closed Connection异常。这是为什么呢?问题出在DatabaseTestCase中。

    ***参数含义
    DatabaseOperation.CLEAN_INSERT;      先删除表中所有,再插入准备的数据
    DatabaseOperation.REFRESH;              使用准备数据更新表,存在则update,不存在则insert
    DatabaseOperation.DELETE;                只删除准备的数据
    DatabaseOperation.DELETE_ALL           清除所有记录
    DatabaseOperation.NONE;                   啥都不做

    java.sql.SQLException: Closed Connection异常
    来看一下DatabaseTestCase的一个关键成员变量tester和有关的一些方法:

    public abstract class DatabaseTestCase extends TestCase
    {
       ......
       private IDatabaseTester tester;
       ......
       protected IDatabaseTester getDatabaseTester() throws Exception {
          if (this.tester == null) {
             this.tester = newDatabaseTester();
          }
          return this.tester;
       }
       ......
       protected IDatabaseTester newDatabaseTester() throws Exception{
          logger.debug("newDatabaseTester()- start");
          // 重载的 getConnection 方法,在 IDatabaseTester 里有一个同名方法。
          // 注意区分。
          final IDatabaseConnection connection = getConnection();
          final IDatabaseTester tester
             = new DefaultDatabaseTester(connection);
          return tester;
       }
       ......
       protected void setUp() throws Exception
       {
          logger.debug("setUp()- start");
          super.setUp();
          final IDatabaseTester databaseTester = getDatabaseTester();
          assertNotNull("DatabaseTesteris not set", databaseTester);
          databaseTester.setSetUpOperation(getSetUpOperation());
          databaseTester.setDataSet(getDataSet());
          databaseTester.onSetup();
       }
       ......
    }

    可见 DatabaseTestCase 内部有一个 IDatabaseTester 接口的实例(tester),实际上所有的测试工作是由它完成的。而DatabaseTestCase的newDatabaseTester方法在生成这个实例的时候用的是DefaultDatabaseTester。传入一个由重载的getConnection方法返回的IDatabaseConnection实例。

    DefaultDatabaseTester记录了这个连接实例后,提供了一个同名的getConnection()方法(不是DatabaseTestCase中被重载的那个getConnection),用来返回它:

    public class DefaultDatabaseTester extends AbstractDatabaseTester
    {
       final IDatabaseConnection connection;
       public DefaultDatabaseTester(final IDatabaseConnection connection){
          this.connection= connection;
       }
       public IDatabaseConnection getConnection() throws Exception {
          return this.connection;
       }
    }

    因为所有的IDatabaseTester实现(包括DefaultDatabaseTester)都继承自AbatractDatabaseTester,这个抽象类有一个统一的执行数据库操作的方法executeOperation,原代码如下:

    private void executeOperation(DatabaseOperation operation) throws Exception
    {
       logger.debug("executeOperation(operation={})- start", operation);
       if(operation != DatabaseOperation.NONE ){
          // IDatabaseTester 的 getConnection 方法,不是重载的那个。
          IDatabaseConnection connection = getConnection();
          try{
              operation.execute(connection, getDataSet() );
          }
          finally{
             closeConnection(connection);
          }
       }
    }

    我们看到每执行完一次操作,数据库连接都会被关闭,所以如果继承DefaultDatabaseTester,将导致只能执行一次数据库操作。
    如果希望在一个TestCase里执行两次操作,我们可以使用另一个基类


    DBTestCase类
    如上面所看到的,问题出在DatabaseTestCase的newDatabaseTester方法返回了一个无法重复利用的DefaultDatabaseTester实例,所以DBTestCase的newDatabaseTester方法代码变更如下:

    protected IDatabaseTester newDatabaseTester() throws Exception {
       return new PropertiesBasedJdbcDatabaseTester();
    }

    它用来生成实例的是 PropertiesBasedJdbcDatabaseTester 类,而不是 DefaultDatabaseTester 。这个类的父类 JdbcDatabaseTester(也继承自 AbstractDatabaseTester)的 getConnection 方法:

    public IDatabaseConnection getConnection() throws Exception
    {
       logger.debug("getConnection() - start");
       if(!initialized ){
          // 注意这个方法,等一下详解
          initialize();
       }
       assertNotNullNorEmpty("connectionUrl", connectionUrl);
       Connection conn = null;
       if(username == null && password == null ){
          conn = DriverManager.getConnection(connectionUrl);
       }else{
          Conn = DriverManager.getConnection(connectionUrl,username,password);
       }
       return new DatabaseConnection( conn, getSchema() );
    }

    可以看到每次调用这个方法,都会新建一个连接,而不是简单的返回我们重载的 getConnection 中返回的连接的引用。这样就避免了 DefaultDatabaseTester 仅仅是简单返回之前的连接而倒置的问题。不过这也意味着用 DBTestCase 就不用我们自己去重载 getConnection 了,因为 DBTestCase 已经实现了这个方法(DatabaseTestCase没有实现):

    protected IDatabaseConnection getConnection() throws Exception {
       logger.debug("getConnection() - start");
       final IDatabaseTester databaseTester = getDatabaseTester();
       assertNotNull( "DatabaseTester is not set",databaseTester);
       return databaseTester.getConnection();
    }

    我们看到DBTestCase的getConnection简单的把这个方法转给JdbcDatabaseTester(IDatabaseTester) 的getConnection。而JdbcDatabaseTester的实现我们在前面已经看到了。

    现在我们把DatabaseTestCase替换成DBTestCase,并且注释掉HelloDBUnit中重载的getConnection(不再需要了,父类DBTestCase中已经实现了该方法。)然后执行,我们又遇到一个异常:

    org.dbunit.AbstractDatabaseTester$AssertionFailedError:driverClass is null

    这是因为PropertiesBasedJdbcDatabaseTester的initialize方法(见上面代码)用来初始化数据库连接参数:

    protected void initialize() throwsException
    {
       logger.debug("initialize() - start");
       setDriverClass(System.getProperty(DRIVER_CLASS));
       setConnectionUrl(System.getProperty(CONNECTION_URL));
       setUsername(System.getProperty(USERNAME));
       setPassword(System.getProperty(PASSWORD));
       // 如果所用测试帐户是 DBA,为了避免出现 AmbiguousTableNameException
       // 异常,必须明确给出 SCHEMA。注意 SCHEMA要大写**
       // setSchema(System.getProperty(SCHEMA));
       super.initialize();
    }

    从这里我们看到DBTestCase需要从系统变量中获得连接参数,所以我们必须修改HelloDBUnit的构造函数,将配置参数读入系统变量:

    public HelloDBUnit() throws IOException
    {
       super();
          props.load(Resources.getResourceAsStream("database.properties"));
          if(null == context)
          {
             context = new ClassPathXmlApplicationContext(
                "classpath:ApplicationContext.xml");
          }
       System.setProperty(
          PropertiesBasedJdbcDatabaseTester.DBUNIT_DRIVER_CLASS,
          props.getProperty("database.connection.driver_class"));
       System.setProperty(
          PropertiesBasedJdbcDatabaseTester.DBUNIT_CONNECTION_URL,
          props.getProperty("database.connection.url"));
       System.setProperty(
          PropertiesBasedJdbcDatabaseTester.DBUNIT_USERNAME,
          props.getProperty("database.connection.username"));
       System.setProperty(
          PropertiesBasedJdbcDatabaseTester.DBUNIT_PASSWORD,
          props.getProperty("database.connection.password"));
    }

    现在可以正确执行了。不过,这依然存在遗憾,既然我们使所有配置参数都文本化了,就不希望看到

       System.setProperty(
          PropertiesBasedJdbcDatabaseTester.DBUNIT_DRIVER_CLASS,
          props.getProperty("database.connection.driver_class"));

    这样的代码,因为如果我们有多个数据源,或改变了配置文件,我们不得不还需要改变测试用例的代码。可以用DataSourceBasedDBTestCase 来解决这个问题。

    DataSourceBasedDBTestCase类
    DataSourceBasedDBTestCase 抽象类要求子类实现 getDataSource 方法以获取外部数据源。

    首先,改变父类为 DataSourceBasedDBTestCase 。然后移除构造函数中关于系统变量的设置。最后加入以下代码:

    @Override
    protected DataSource getDataSource(){
       return(DataSource)context.getBean("dataSource");
    }

    org.dbunit.database.AmbiguousTableNameException异常
    ** 很遗憾, DataSourceBasedDBTestCase 没有提供设置 SCHEMA 的方法,虽然它的成员变量 DataSourceBasedDBTester 通过继承了 AbstractDatabaseTester 而提供了 setSchema 方法,但是因为所有的 IDatabaseTester 都是 Private 变量,因此实际上我们无法访问到它的 setSchema。所以如果使用 DataSourceBasedDBTestCase ,除非重载 setUp 和tearDown方法,否则就不能有DBA权限。

    protected void setUp() throws Exception
    {
       DataSource dataSource = getDataSource();
       Connection connection = dataSource.getConnection();
       IDatabaseConnection dbUnitCon
          = new DatabaseConnection(connection, "SYSTEM");
       if(getDataSet() != null)
       {
          try
          {
              getSetUpOperation().execute(dbUnitCon, getDataSet());
          }
          finally
          {
              if(connection!= null)
                 connection.close();
          }
       }
    }
    protected void tearDown() throws Exception
    {
       DataSource dataSource = getDataSource();
       Connection connection = dataSource.getConnection();
       IDatabaseConnection dbUnitCon
          = new DatabaseConnection(connection, "SYSTEM");
       if(getDataSet()!= null)
       {
          try
          {
              getTearDownOperation().execute(dbUnitCon,getDataSet());
          }
          finally
          {
              if(connection!= null)
                 connection.close();
          }
       }
    }


    支持事务回滚
    虽然DBUnit提供了一些方法让我们可以在测试开始和结束时清理数据库,但是有时候依然不能满足需求,比如在上面的代码中,我们在执行阶段插入了一条记录(见testCreateAccount方法),这种不是在种子文件中的额外数据,测试结束后除非在tearDown中返回DatabaseOperation.DELETE_ALL,否则是不会被自动删除的,可是如果删除全部数据,那么又有可能删掉了不希望删掉的数据。Spring提供了一个AbstractTransactionalDataSourceSpringContextTests测试类,这个类可以在测试结束后回滚数据库,可是DBUnit没有提供类似的机制,所以我们要进一步手工扩展测试用例,以加入类似功能。

    修改ApplicationContext.xml
    首先,修改Spring的配置文件ApplicationContext.xml,加入以下配置:

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
       <property name="dataSource">
          <ref bean="dataSource"/>
       </property>
    </bean>
    <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
       <property name="transactionManager">
          <ref bean="transactionManager"/>
       </property>
       <property name="transactionAttributes">
          <props>
              <prop key="*">PROPAGATION_REQUIRED</prop>
          </props>
       </property>
    </bean>

    如果希望业务类也支持事务处理可以加入以下配置,否则可以略过:

    <bean id="accountManagerTarget" class="com.wang.dbunit.AccountManager">
       <property name="sqlMapClient" ref="sqlMapClient"/>
    </bean>
    <bean id="accountManager" class="org.springframework.aop.framework.ProxyFactoryBean">
       <property name="proxyInterfaces">
          <value>
              com.wang.dbunit.IAccountManager
          </value>
       </property>
       <property name="interceptorNames">
          <list>
              <idref local="transactionInterceptor" />
              <idref local="accountManagerTarget" />
          </list>
       </property>
    </bean>

    以上配置只作用于使业务类,因为我们的测试用例类“HelloDBUnit.java”没有出现在配置文件中,更没有设置任何拦截器,所以测试用例对数据库的所有操作(插入、清除测试数据)目前都不在拦截范围。我们必须在测试用例中手工为它加入事务处理,才可以达到我们的目的。

    添加事务管理代码
    添加以下属性和方法:

    private TransactionStatus ts = null;
    private DataSourceTransactionManager transactionManager = null;
    ......
    protected void setUpTransaction()
    {
       transactionManager
          =(DataSourceTransactionManager)context.getBean(
             "transactionManager");
       TransactionDefinition td = new DefaultTransactionDefinition();
       ts = transactionManager.getTransaction(td);
    }
    protected void tearDownTransaction(boolean commit)
    {
       if(commit)
       {
          transactionManager.commit(ts);
       }
       else
       {
          transactionManager.rollback(ts);
       }
    }

    修改setUp和tearDown方法:

    protected void setUp() throws Exception
    {
       setUpTransaction();
       DataSourced ataSource = getDataSource();
       // 替换 Connection connection = dataSource.getConnection();
       Connection connection = DataSourceUtils.getConnection(dataSource);
       IDatabaseConnection dbUnitCon
          = new DatabaseConnection(connection, "SYSTEM");
       if(getDataSet()!= null)
       {
          try
          {
             getSetUpOperation().execute(dbUnitCon, getDataSet());
          }
          finally
          {
             if(connection!= null)
             {
                // 替换 connection.close();
                DataSourceUtils.releaseConnection(
                   connection, dataSource);
              }
          }
       }
    }
    protected void tearDown() throws Exception
    {
       DataSource dataSource = getDataSource();
       // 替换 Connection connection = dataSource.getConnection();
       Connection connection = DataSourceUtils.getConnection(dataSource);
       IDatabaseConnection dbUnitCon
          = new DatabaseConnection(connection, "SYSTEM");
       if(getDataSet() != null)
       {
          try
          {
              // 如果不希望回滚数据,传入 true 参数。
              tearDownTransaction(false);
              getTearDownOperation().execute(dbUnitCon, getDataSet());
          }
          finally
          {
             if(connection!= null)
             {
                // 替换 connection.close();
                DataSourceUtils.releaseConnection(
                   connection, dataSource);
              }
          }
       }
    }

    最后修改getTearDownOperation,用 DELETE 替换 DELETE_ALL:

    protected DatabaseOperation getTearDownOperation() throws Exception
    {
        return DatabaseOperation.DELETE;
    }

    现在在表中随便添加一些记录,然后执行我们的测试用例,执行完后,手工添加的数据没有受到任何影响。


    最后一点提示
    因为每一个 testXxx 方法时都会初始化一个测试用例,所以每执行一次 testXxxx 方法都会导致 setUp 和 tearDown 方法被调用一次,而本例的事务定义也都由 setUp 开始, tearDown 结束,也就意味着不同测试方法(同一测试用例)之间处于不同的事务范围,导致数据操作结果无法共享(如果每次 tearDown 都回滚数据)。比如在 testInsert 方法中插入数据,在 testSelect 无法获得。要解决这个问题,就要适当的修改对 setUpTransaction 和 dearDownTransaction 的调用,使得事务可以在全局范围内被多个 test 方法共享。
    E:\work\bpm2_v1_00\test.xml:171: org.dbunit.dataset.NoSuchTableException: VERSION_INFO
    DBunit 要求schema 大写 google_protectAndRun("render_ads.js::google_render_ad", google_handleError, google_render_ad);

    转自:http://zscomehuyue.javaeye.com/blog/846168

  • Junit中的参数化测试

    2011-03-04 18:06:40

    Junit4中的Parameterized Test通过“@RunWith” and "@Parameter"实现,参数化测试的一些限制:
     
    1.设置参数数据的方法需要@Parameters
    2.该必须返回List[]
    3.该方法必须为public static
    4.参数化测试的类必须有Parameterized测试运行器修饰
     
    参考代码如下:
    import java.util.Arrays;
    import java.util.Collection;
     
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.junit.runners.Parameterized;
    import org.junit.runners.Parameterized.Parameters;
     

    @RunWith(value = Parameterized.class)
    public class JunitTest6 {
     
      private int number;
     
      public JunitTest6(int number) {
         this.number = number;
      }
     
      @Parameters
      public static Collection<Object[]> data() {
        Object[][] data = new Object[][] { { 1 }, { 2 }, { 3 }, { 4 } };
        return Arrays.asList(data);
      }
     
      @Test
      public void pushTest() {
        System.out.println("Parameterized Number is : " + number);
      }
    }
    运行结果:
     
  • 单元测试相关问题与思考

    2011-02-11 15:18:45

       1、单元测试验证粒度:单元测试时,需要验证到什么程度,比如:
          一个函数是查询数据的,参数主要有查询条件、是否分页、每页条数、第几页等,单元测试时,验证结果与提交的条件匹配,同时是否需要验证数据分页是否正确?如验证,该如何验证获取的数据是第X页的,当然,肯定是有办法验证的,但是验证是否必要,写这样的代码投入产出比是否高?
       2、单元测试脚本的划分与组织
          假设测试源码的某类包含所有的操作,如添加、修改、删除,写单元测试脚本时,是否针对此类的测试均写在一个测试类中,还是添加、修改、删除放到不同的测试类中,通过testsuite组织?
       3、测试垃圾数据的清除?
          一般情况下,单元测试数据与实际应用数据差别较大,执行完测试后,数据应该清除,当被测试类不提供删除方法时,如何清除垃圾数据?
       4、如何提高单元测试效率,如何验证单元测试的效果?
          这次单元测试发现一个bug,但该bug本质是设计问题,除了bug,是否有其他方式验证单元测试脚本的效果呢?
  • 单元测试需注意的一些方面

    2011-02-11 15:14:06

    •  尽量使用符合xunit框架方式编写测试脚本,便于用例复用,测试执行和结果查看
    • 尽量用Assert验证结果,不要通过查看输出的方式验证结果
    • 单元测试用例设计和功能用例设计一样,也需要从正常、边界和异常输入等方面考虑
    • 尽量覆盖所有的逻辑分支
    • 对于需要通过修改配置文件等方式才能验证的异常,建议验证后,用@Ignore 标注,不用每次都执行
    • 单元测试时尽量不要留下垃圾数据,保持环境的整洁
    • 单元测试验证力度尽量与实现一致
    • 尽量降低测试用例间的耦合度
    • 如果需要,可以通过@Test(timeout=5000)的方式测试单个方法执行时间
    • 建议尽量通过testSuite方式组织测试用例
    • 注意Setup,Teardown/@Before, @After是执行每个测试用例前会执行Setup/@before 相关的方法, 执行完成后执行Teardown/ @after @BeforeClass是执行所有的测试用例前执行 ,@Afterclass 是执行所有的测试用例后执行
  • phpunit和php单元测试体验--(四)

    2008-11-10 11:39:47

     自己感觉单元测试过程中需注意以下几个方面:

     1、各测试用例(testXXX)的数据耦合度要低。如果耦合度较高的话,如果前一个测试用例失败的话,之后的测试用例会出现error,无法继续。

     2、测试用例尽量覆盖各逻辑分支,保证测试覆盖率。

     3、测试用例设计和非单元测试的用例设计方法一致,针对一个方法测试需包含:正常测试、异常测试和边界测试等。

     4、合理的利用setUp合tearDown, 每执行一个testXX时,都首先运行setUP,执行完testXX后,都会运行tearDown, 因此可以把各个测试用例开始都需要初始化且初始化值一样的参数放到setUP中,而执行完后需要unset的参数放到tearDown函数中。

     5、用phpunit skelon 生成测试框架时,只有.php文件才可以,测试其他格式的文件如 inc格式文件无法生成的。可以修改文件名后在生成。

  • phpunit和php单元测试体验--(三)

    2008-11-10 10:53:01

    写完单元测试脚本后,下一步就是执行测试,最简单的命令是:phpunit xxx.php 更加详细的命令如下: phpunit --log-xml(log 文件格式) 文件路径/文件名 --coverage-html(代码覆盖文件格式) 文件路径/文件名 单元测试用例类 单元测试文件 如:phpunit --log-xml path/createAlbumlog --coverage-html path/logs/ createAlbumtest createAlbum.php 命令执行过程中,.--表示成功,e--代表error, f--代表失败, 执行完成后会显示测试用例成功、失败和出错情况。也可以通过查看log查看测试情况,coverage文件可以查看单元测试脚本对测试文件代码覆盖率情况。
  • phpunit和php单元测试体验--(二)

    2008-11-06 10:21:57

    写phpunit测试脚本时,可以通过phpunit Skeleton 生成单元测试的脚本结构,然后在结构中填写测试用例。

    如果不用skeleton生成,自己写也很方便。

    phpunit的测试文件包含以下部分:

    引入必要的文件

    •    require 'x.php'--被测试的php文件
    •    rquire 'phpunitpath\phpunit\framework'---  phpunit的framewok文件

    创建一个从phpunit frame继承的类

    class XXtest extends PHPUnit_Framework_TestCase

     [初始化公共的参数]

      public $para1='';

      public $para2='';

    写setup和teardown函数

     public function setUp() {}

     public function tearDown() {}

     注意,每运行一个test的函数,均会首先运行setup,每运行完一个函数,均会运行teardown. 写这2个函数时需要注意。

     下一步就是写测试函数了,比如测试

      public function sum(a,b)

      {return a+b;}

     写测试函数时和测试用例是对应的,我们首先测试正常情况下求和是否正确,testXX函数可以如下:

      public function testSumNormal ()                                                       
         {

      coding....

      assertSame(expectresult,actualcode,message)

    }

     所有的测试函数均已test开头,一般通过assert..验证结果是否正确。

     

  • phpunit和php单元测试体验--(一)

    2008-11-05 13:59:34

      前段时间的一个项目中,由于开发工程师时间比较紧,没时间进行单元测试。所以我临时替补作单元测试。个人对phpunit了解不是很深入,本文只是对phpunit框架做简要介绍并写一些使用的体验。有误之处还请多多指证。

      和其他的Xunit测试框架一样,phpunit框架主要分为以下部分:

    •   TestCase --测试用例
    •   TestSuite --测试套件(我习惯称其为测试用例集)
    •   Assert--断言(我习惯称其为验证方式)
    •   TestResult--测试结果

      对于testcase,phpunit中除了一般的testcase,还包含了Incompletetestcase(未完成的)和skiptestcase(跳过的测试用例)。

      testcase是单元测试的基本组成部分,每个testcase均包含以下部分:

    • Setup()---测试前的初始化,执行每个testMethod前均会执行
    • Teardown()--测试完成后恢复,执行完每个testMethod后均会执行
    • testMethod()--测试方法(测试点)

     

Open Toolbar