日期:2014-05-20 浏览次数:21037 次
当时没有做单元测试、自动化集成测试,但是有人工的验收测试,关键业务有压力测试。也许有人会侧目,但是这是公司与项目的现状……期间曾经有领导想推行单元测试,但是未推行成功,就其原因,我觉得有如下几条:
随着公司业务的飞速发展,系统使用的场景越来越复杂,对以用功能的升级改造也越来越频繁,代码重构越来越多,所以测试人员的压力很大,由功能升级或重构导致的bug也多了起来。此时通过自动化的测试脚本来提高开发质量与系统稳定性就越来越重要了。
所以根据以上几点,我们选择基于Spring TestContext Framework做集成测试。
根据系统的事务特点,我们需要使用脱离应用服务器的JNDI数据源,而不能使用轻量级的数据源(如DBCP),因为JtaTransactionManager是不支持的。通过查阅Spring的文档,我们使用Spring的JotmFactoryBean结合ow2-jotm的分发版本中的StandardXADataSource来构建脱离应用服务器的JNDI数据源,配置如下:
<bean id="jotm" class="org.springframework.transaction.jta.JotmFactoryBean" /> <bean id="aInnerDataSource" class="org.enhydra.jdbc.standard.StandardXADataSource" destroy-method="shutdown"> <property name="transactionManager" ref="jotm" /> <property name="driverName" value="oracle.jdbc.driver.OracleDriver" /> <property name="url" value="jdbc:oracle:thin:@192.168.1.1:15211:a" /> <property name="user" value="xxx" /> <property name="password" value="xxx" /> </bean> <bean id="aDataSource" class="org.enhydra.jdbc.pool.StandardXAPoolDataSource" destroy-method="shutdown"> <property name="dataSource" ref="aInnerDataSource"/> <property name="user" value="xxx"/> <property name="password" value="xxx"/> <property name="maxSize" value="5"/> </bean> <bean id="bInnerDataSource" class="org.enhydra.jdbc.standard.StandardXADataSource" destroy-method="shutdown"> <property name="transactionManager" ref="jotm" /> <property name="driverName" value="oracle.jdbc.driver.OracleDriver" /> <property name="url" value="jdbc:oracle:thin:@192.168.1.2:15212:b" /> <property name="user" value="xxx" /> <property name="password" value="xxx" /> </bean> <bean id="bDataSource" class="org.enhydra.jdbc.pool.StandardXAPoolDataSource" destroy-method="shutdown"> <property name="dataSource" ref="bInnerDataSource"/> <property name="user" value="xxx"/> <property name="password" value="xxx"/> <property name="maxSize" value="5"/> </bean> <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="userTransaction" ref="jotm" /> <property name="allowCustomIsolationLevels" value="true" /> </bean>
?以上的配置,可以参考Spring JotmFactoryBean的doc,里面有比较详尽的说明,这里有两点说明:
Spring TestContext Framework支持JUnit3.8、JUnit4和TestNG,这里我们以JUnit4为例。首先让我们来看一下Spring基于JUnit4为我们提供的基类:AbstractTransactionalJUnit4SpringContextTests、AbstractJUnit4SpringContextTests。通过查阅文档与代码,我们知道,AbstractTransactionalJUnit4SpringContextTests为其子类提供了事务能力以及简单的数据初始化接口,而其本身就是AbstractJUnit4SpringContextTests的子类。在大多数应用场景中,直接继承AbstractTransactionalJUnit4SpringContextTests,从而获得事务能力是一个不错的选择。
由于项目中使用了Hibernate,默认的FlushModel是"manual"或"Never",所以对于Inert语句的Flush会在事务提交的时候执行。由于测试代码中拥有@Transactional以及事务的传播策略,所以当Service中的事务增强方法执行结束之后,事务并未像在生产环境中那样提交,则Hibernate也没有flush。导致我们想在一个测试单元中验证事务提交后的结果变的不再可行。
基于此,我们有必要设置Hibernate,希望它立即提交,或者我们手动提交,以保证Session的Flush。为了不修改我们的代码以及尽可能的模拟代码在生产环境中的执行,我们通过AOP做一个后置处理,即在每次业务Service执行完,将SessionFluash:
public class SessionFlushingAfterAdvice implements AfterReturningAdvice { private List<SessionFactory> sessionFactories; @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { for (SessionFactory sessionFactory : sessionFactories) { sessionFactory.getCurrentSession().flush(); } } public List<SessionFactory> getSessionFactories() { return sessionFactories; } public void setSessionFactories(List<SessionFactory> sessionFactories) { this.sessionFactories = sessionFactories; } }
?
这样我们在Insert之后,可以去校验对象的状态,并且在测试完成后,事务会自动回滚。
?
在初始化测试的数据环境时,我们经常会通过AbstractTransactionalJUnit4SpringContextTests的simpleJdbcTemplate执行一些写操作,以满足测试环境。如果通过simpleJdbcTemplate Insert了一条数据,之后在测试脚本中又对该数据做Update,造成数据库的死锁。
所以如果在初始化数据时需要Insert操作,请不要在事务环境下处理。
?在Acegi中,我们会用SecurityContextHolder从当前用户的上下文环境中获得用户信息,虽然Acegi提供了TestAuthenticationToken,但是模拟的比较简单,例如无法通过WebAuthenticationDetails获取用户当前客户的IP。而mock静态变量本身较为复杂,业务代码中有大量的静态方法也不利于测试,于是我们可以通过一个单例Service将其包装一下:
public interface UserSecurityContextHolder { /** * * <li>获取当前登录用户</li> * */ public User getCurrentUser(); /** * * <li>获取用户的登录IP</li> * */ public String getLoginIP(); } public class DefaultUserSecurityContextHolder implements UserSecurityContextHolder { @Override public User getCurrentUser() { User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (null == user) throw new AuthenticationServiceException("authentication failure,no user in the context!"); return user; } @Override public String getLoginIP() { WebAuthenticationDetails details = (WebAuthenticationDetails) SecurityContextHolder .getContext() .getAuthentication() .getDetails(); return details.getRemoteAddress(); } } public class UserSecurityContextHolderForTest implements UserSecurityContextHolder { @Autowired private HibernateSessionWrapper wrapper; @Override public User getCurrentUser() { return wrapper.get(User.class, 1l); } @Override public String getLoginIP() { return "127.0.0.1"; } }
?在开发业务代码中,通过Spring注入这个service获取当前登陆用户的信息。在测试环境中,只要配置一个用于测试的实现即可。
对于测试,我们才刚刚上路,单元测试并非项目成败的关键,但是拥有之后却会提高代码的质量,进而提高项目成功的几率(废话)。而测试会促使我们去编写更健壮的代码,良好的测试脚本,让我们有重构的勇气,增强程序员的信心,这种影响无疑是无形和重要的。
?
?
<?xml version="1.0" encoding="gb2312"?> <poolman> <datasource> <dbname>bspf</dbname> <loadmetadata>true</loadmetadata> <jndiName>jdbc/mysql-ds</jndiName> <driver>oracle.jdbc.driver.OracleDriver</driver> <url>jdbc:oracle:thin:@//172.16.17.219:1521/orcl</url> <username>testmq</username> <password>testmq</password> <txIsolationLevel>READ_COMMITTED</txIsolationLevel> <nativeResults>true</nativeResults> <poolPreparedStatements>false</poolPreparedStatements> <initialConnections>2</initialConnections> <minimumSize>0</minimumSize> <maximumSize>10</maximumSize> <!-- 控制connection达到maximumSize是否允许再创建新的connection true:允许,缺省值 false:不允许 --> <maximumSoft>false</maximumSoft> <!-- 是否检测超时链接(事务超时链接) true-检测,如果检测到有事务超时的链接,系统将强制回收(释放)该链接 false-不检测,默认值 --> <removeAbandoned>true</removeAbandoned> <!-- 链接使用超时时间(事务超时时间) 单位:秒 --> <userTimeout>50</userTimeout> <!-- 系统强制回收链接时,是否输出后台日志 true-输出,默认值 false-不输出 --> <logAbandoned>true</logAbandoned> <!-- 数据库会话是否是readonly,缺省为false --> <readOnly>false</readOnly> <!-- 对应属性:timeBetweenEvictionRunsMillis the amount of time (in milliseconds) to sleep between examining idle objects for eviction --> <skimmerFrequency>10</skimmerFrequency> <!-- 对应于minEvictableIdleTimeMillis 属性: minEvictableIdleTimeMillis the minimum number of milliseconds an object can sit idle in the pool before it is eligable for evcition 单位:秒 空闲链接回收时间,空闲时间超过指定的值时,将被回收 --> <connectionTimeout>60</connectionTimeout> <!-- numTestsPerEvictionRun the number of idle objects to examine per run within the idle object eviction thread (if any) 每次回收的链接个数 --> <shrinkBy>5</shrinkBy> <!-- /** * 检测空闲链接处理时,是否对空闲链接进行有效性检查控制开关 * true-检查,都检查到有无效链接时,直接销毁无效链接 * false-不检查,缺省值 */ --> <testWhileidle>true</testWhileidle> <!-- 定义数据库主键生成机制 缺省的采用系统自带的主键生成机制, 外步程序可以覆盖系统主键生成机制 由值来决定 auto:自动,一般在生产环境下采用该种模式, 解决了单个应用并发访问数据库添加记录产生冲突的问题,效率高,如果生产环境下有多个应用并发访问同一数据库时,必须采用composite模式 composite:结合自动和实时从数据库中获取最大的主键值两种方式来处理,开发环境下建议采用该种模式, 解决了多个应用同时访问数据库添加记录时产生冲突的问题,效率相对较低, 如果生产环境下有多个应用并发访问同一数据库时,必须采用composite模式 --> <keygenerate>composite</keygenerate> <!-- 请求链接时等待时间,单位:秒 客服端程序请求链接等待时间超过指定值时,后台包等待超时异常 --> <maxWait>60</maxWait> <!-- 链接有效性检查sql语句 --> <validationQuery>select 1 from dual</validationQuery> <autoprimarykey>false</autoprimarykey> <showsql>false</showsql> </datasource> <datasource> <dbname>query</dbname> <loadmetadata>true</loadmetadata> <jndiName>jdbc/oracle-query</jndiName> <driver>oracle.jdbc.driver.OracleDriver</driver> <url>jdbc:oracle:thin:@//172.16.17.219:1521/orcl</url> <username>query</username> <password>query</password> <txIsolationLevel>READ_COMMITTED</txIsolationLevel> <nativeResults>true</nativeResults> <poolPreparedStatements>false</poolPreparedStatements> <initialConnections>2</initialConnections> <minimumSize>0</minimumSize> <maximumSize>10</maximumSize> <!-- 控制connection达到maximumSize是否允许再创建新的connection true:允许,缺省值 false:不允许 --> <maximumSoft>false</maximumSoft> <!-- 是否检测超时链接(事务超时链接) true-检测,如果检测到有事务超时的链接,系统将强制回收(释放)该链接 false-不检测,默认值 --> <removeAbandoned>true</removeAbandoned> <!-- 链接使用超时时间(事务超时时间) 单位:秒 --> <userTimeout>50</userTimeout> <!-- 系统强制回收链接时,是否输出后台日志 true-输出,默认值 false-不输出 --> <logAbandoned>true</logAbandoned> <!-- 数据库会话是否是readonly,缺省为false --> <readOnly>false</readOnly> <!-- 对应属性:timeBetweenEvictionRunsMillis the amount of time (in milliseconds) to sleep between examining idle objects for eviction --> <skimmerFrequency>10</skimmerFrequency> <!-- 对应于minEvictableIdleTimeMillis 属性: minEvictableIdleTimeMillis the minimum number of milliseconds an object can sit idle in the pool before it is eligable for evcition 单位:秒 空闲链接回收时间,空闲时间超过指定的值时,将被回收 --> <connectionTimeout>60</connectionTimeout> <!-- numTestsPerEvictionRun the number of idle objects to examine per run within the idle object eviction thread (if any) 每次回收的链接个数 --> <shrinkBy>5</shrinkBy> <!-- /** * 检测空闲链接处理时,是否对空闲链接进行有效性检查控制开关 * true-检查,都检查到有无效链接时,直接销毁无效链接 * false-不检查,缺省值 */ --> <testWhileidle>true</testWhileidle> <!-- 定义数据库主键生成机制 缺省的采用系统自带的主键生成机制, 外步程序可以覆盖系统主键生成机制 由值来决定 auto:自动,一般在生产环境下采用该种模式, 解决了单个应用并发访问数据库添加记录产生冲突的问题,效率高,如果生产环境下有多个应用并发访问同一数据库时,必须采用composite模式 composite:结合自动和实时从数据库中获取最大的主键值两种方式来处理,开发环境下建议采用该种模式, 解决了多个应用同时访问数据库添加记录时产生冲突的问题,效率相对较低, 如果生产环境下有多个应用并发访问同一数据库时,必须采用composite模式 --> <keygenerate>composite</keygenerate> <!-- 请求链接时等待时间,单位:秒 客服端程序请求链接等待时间超过指定值时,后台包等待超时异常 --> <maxWait>60</maxWait> <!-- 链接有效性检查sql语句 --> <validationQuery>select 1 from dual</validationQuery> <autoprimarykey>false</autoprimarykey> <showsql>false</showsql> </datasource> <datasource external="true"> <dbname>mq</dbname> <externaljndiName>jdbc/mysql-ds</externaljndiName> <showsql>false</showsql> </datasource> </poolman>
package com.frameworkset.common; import javax.transaction.RollbackException; import org.junit.Test; import com.frameworkset.common.poolman.DBUtil; import com.frameworkset.orm.transaction.TransactionManager; public class TestMutiDBTX { public static void testMutiDBTX() { TransactionManager tm = new TransactionManager(); try { tm.begin(); DBUtil db = new DBUtil(); db.executeDelete("bspf","delete from table1 where id=1"); db.executeUpdate("query","update table1 set value='test' where id=1"); tm.commit(); DBUtil.debugStatus(); } catch(Exception e) { try { tm.rollback(); } catch (RollbackException e1) { e1.printStackTrace(); } } } @Test public void testMutiDBButSampleDatabaseTX() { TransactionManager tm = new TransactionManager(); try { tm.begin(); DBUtil db = new DBUtil(); db.executeDelete("bspf","delete from table1 where id=1"); db.executeUpdate("mq","update table1 set value='test' where id=1"); tm.commit(); DBUtil.debugStatus(); } catch(Exception e) { try { tm.rollback(); } catch (RollbackException e1) { e1.printStackTrace(); } } DBUtil.debugStatus(); } }