Levit应用中存在Jtester和DBUnit两种单元测试框架,在信联项目中发现2个框架编写的单元测试同时跑会发生冲突导致单元测试不过。 当时由于项目时间紧张采取了临时解决方法,Jtester与dbunit单元测试分开跑来规避了此问题。。。没有解决根本问题,现在就一步一步的去探索究 竟是什么问题引起的。。。
执行命令:
mvn clean compile test
错误信息如下:
为什么会有46个错误呀,好恐怖呀,打开TestSuite.txt看一下具体是什么问题,部分错误信息如下:
testFind(com.alibaba.china.levit.biz.guarantee.dal.dao.test.AcFreezeDAOTest) Time elapsed: 6.934 sec <<< FAILURE!
junit.framework.AssertionFailedError: null
at com.alibaba.china.levit.biz.guarantee.dal.dao.test.AcFreezeDAOTest.testFind(AcFreezeDAOTest.java:127)
testCountFreezeAndFreezeForever(com.alibaba.china.levit.biz.guarantee.dal.dao.test.AcFreezeDAOTest) Time elapsed: 7.188 sec <<< FAILURE!
junit.framework.AssertionFailedError: expected:<8> but was:<121>
at com.alibaba.china.levit.biz.guarantee.dal.dao.test.AcFreezeDAOTest.testCountFreezeAndFreezeForever(AcFreezeDAOTest.java:283)
testQueryBySearchParam(com.alibaba.china.levit.biz.guarantee.dal.dao.test.AcFreezeDAOTest) Time elapsed: 7.276 sec <<< FAILURE!
junit.framework.AssertionFailedError: expected:<1> but was:<0>
at com.alibaba.china.levit.biz.guarantee.dal.dao.test.AcFreezeDAOTest.testQueryBySearchParam(AcFreezeDAOTest.java:300)
testCountTrade(com.alibaba.china.levit.biz.guarantee.dal.dao.test.AcFreezeDAOTest) Time elapsed: 7.353 sec <<< FAILURE!
junit.framework.AssertionFailedError: expected:<2> but was:<0>
at com.alibaba.china.levit.biz.guarantee.dal.dao.test.AcFreezeDAOTest.testCountTrade(AcFreezeDAOTest.java:312)
看以上的报误信息大概可以分析出来是null值导致单元测试不过的。。。
因为只有通过mvn test 才能重现这个问题,打开 mvn test远程调试端口,一步步跟...
1、方法一:
mvn -Dmaven.surefire.debug="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -Xnoagent -Djava.compiler=NONE"
test
2、方法二:
修改pom.xml文件,如下代码:
<configuration>
<argLine>-javaagent:"${settings.localRepository}/com/alibaba/external/test.jmockit/0.997/test.jmockit-0.997.jar"
</argLine>
<junitArtifactName>com.alibaba.external:test.junit</junitArtifactName>
<testNGArtifactName>com.alibaba.external:test.testng.jdk15</testNGArtifactName>
<debugForkedProcess>true
</debugForkedProcess>
</configuration>
默认端口为:5005
http://maven.apache.org/plugins/maven-surefire-plugin/examples/debugging.html
AcFreezeDAOTest代码如下:
public
class AcFreezeDAOTest extends
DbUnitSpringTransactionalTestCase {
private
static
final
String
AC_ACFREEZE_SET = "dbunit/acFreezeSet001.xml"
;
private
AcFreezeDAO acFreezeDAO;
/**
* 测试 查找赔付金记录
*
* @throws
Exception
*/
public
void testFind() throws
Exception {
/////////////关键部分代码,根据xml配置文件构造一个数据集到内存中。
setUpDataSet("dbunit/acFreezeSet.xml"
);
AcFreezeDO acFreezeDO = null
;
acFreezeDO = this
.acFreezeDAO.find(2L);
Assert.assertNotNull(acFreezeDO);
Assert.assertEquals(acFreezeDO.getAliFreezeMoney().getCent(), 34);
Assert.assertEquals(acFreezeDO.getMemberFreezeMoney().getCent(), 344);
Assert.assertEquals(acFreezeDO.getBuyerMemberId(), "maomaotest03"
);
Assert.assertEquals(acFreezeDO.getSellerMemberId(), "maomaotest02"
);
Assert.assertEquals(acFreezeDO.getTradeNo(), "000002342342"
);
Assert.assertEquals(acFreezeDO.getTradeType(), "alipay001"
);
Assert.assertEquals(acFreezeDO.getGntFundStatus(), "FREE"
);
Assert.assertEquals(acFreezeDO.getIsFreezeCredit(), FreezeCreditState.FREEZE_CREDIT.getState());
Assert.assertEquals(acFreezeDO.getId().longValue(), 2L);
Assert.assertEquals(DateUtil.toLocaleString(acFreezeDO.getGmtActualUnfreeze(), DATE_FORMAT), "2010-01-20"
);
Assert.assertEquals(DateUtil.toLocaleString(acFreezeDO.getGmtAgreedUnfreeze(), DATE_FORMAT), "2010-01-28"
);
Assert.assertEquals(DateUtil.toLocaleString(acFreezeDO.getGmtCreate(), DATE_FORMAT), "2010-01-25"
);
Assert.assertEquals(DateUtil.toLocaleString(acFreezeDO.getGmtModified(), DATE_FORMAT), "2010-01-25"
);
}
public
AcFreezeDAO getAcFreezeDAO() {
return
acFreezeDAO;
}
public
void setAcFreezeDAO(AcFreezeDAO acFreezeDAO) {
this
.acFreezeDAO = acFreezeDAO;
}
}
DbUnitSpringTransactionalTestCase 类代码 :
public
class DbUnitSpringTransactionalTestCase extends
SpringTransactionalBaseTestCase {
/**
* 设置数据源
*/
SchemaAwareDataSourceProxy dataSource;
/**
* dbunit dbUnitconn初始化,dbunit和数据库交互使用的连接。
*/
public
IDatabaseConnection dbUnitConn;
/**
* 创建之前需要先初始化dbunit 由spring容器自动创建。
*/
protected
void onSetUpInTransaction() throws
Exception {
super
.onSetUpInTransaction();
// dbUnit使用的数据源
if
(dataSource == null
) {
dataSource = (SchemaAwareDataSourceProxy) this
.applicationContext.getBean("dataSource"
);
}
// 初始化dbUnit连接
initDbunit();
setAnotationUpdataSet();
}
/**
* 初始化dbUnit dbUnitconnection. 数据准备准备好连接基础。
*
* @throws
Exception
*/
protected
void initDbunit() throws
Exception {
if
(StringUtil.isNotBlank(dataSource.getDbSchema())) {
dbUnitConn = new
DatabaseConnection(DataSourceUtils.getConnection(dataSource), dataSource.getDbSchema());
} else
{
dbUnitConn = new
DatabaseConnection(DataSourceUtils.getConnection(dataSource));
}
}
/**
* 初始化数据表 传入需要初始化的数据xml文件。
*
* @param file
* @throws
Exception
*/
protected
void setUpDataSet(String
file) {
try
{
IDataSet dataset = new
FlatXmlDataSet(new
ClassPathResource(file).getFile());
DatabaseOperation.CLEAN_INSERT.execute(dbUnitConn, dataset);
} catch
(Exception e) {
e.printStackTrace();
Assert.fail();
}
}
public
IDatabaseConnection getDbUnitConn() {
return
dbUnitConn;
}
public
void setDataSource(SchemaAwareDataSourceProxy dataSource) {
this
.dataSource = dataSource;
}
}
SpringTransactionalBaseTestCase 类部分代码:
public
class SpringTransactionalBaseTestCase extends
AbstractTransactionalDataSourceSpringContextTests {
/**
* 传入applicationContext配置文件,配置文件里面是你需要测试的bean和其依赖的bean. 默认从applicationContext.xml取。如果需要替换,子类可以覆盖。
*/
protected
String
[] getConfigLocations() {
return
new
String
[] { "applicationContext.xml"
};
}
}
applicationContext.xml 文件内容:
<bean id="dataSource"
class="com.alibaba.pivot.common.test.SchemaAwareDataSourceProxy"
destroy-method="close"
>
<property name="driverClassName"
value="com.alibaba.china.jdbc.SimpleDriver"
/>
<property name="url"
value="jdbc:oracle:thin:@10.20.36.18:1521:ocndb"
/>
<property name="username"
value="alibaba"
/>
<property name="password"
value="ca"
/>
<property name="dbSchema"
value="ALIBABA"
/>
<property name="clientEncoding"
value="GBK"
/>
<property name="serverEncoding"
value="ISO-8859-1"
/>
</bean>
<!-- smart ibatis sessionFactoryManager定义。 -->
<bean id="sessionFactoryManager"
class="com.alibaba.ibatis.IBatisSessionFactoryManager"
>
<property name="ormConfig"
>
<map>
<entry key="MAIN_DATASOURCE"
>
<ref local="sqlMapClient"
/>
</entry>
</map>
</property>
<property name="transactionTemplates"
>
<map>
<entry key="MAIN_DATASOURCE"
>
<ref local="transactionTemplate"
/>
</entry>
</map>
</property>
<property name="daoDriverClass"
value="com.alibaba.ibatis.smart.advance.AdvanceSmartIBatisDao"
/>
</bean>
<bean id="acFreezeDAO"
class="com.alibaba.china.credit.dal.guarantee.dao.AcFreezeDAO"
/>
acFreezeSet.xml 文件内容:
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<CREDIT_GNT_AC_FREEZE ID="1"
SELLER_MEMBER_ID="maomaotest02"
BUYER_MEMBER_ID="maomaotest03"
TRADE_NO="00000000001"
TRADE_TYPE="alipay"
MEMBER_FREEZE_MONEY="344"
ALI_FREEZE_MONEY="34"
GMT_AGREED_UNFREEZE="2010-01-28"
GMT_ACTUAL_UNFREEZE="2010-01-20"
GNT_FUND_STATUS="FREE"
GMT_CREATE="2010-01-25"
GMT_MODIFIED="2010-01-25"
IS_FREEZE_CREDIT="N"
SOURCE_DETAIL="234234234"
/>
<CREDIT_GNT_AC_FREEZE ID="2"
SELLER_MEMBER_ID="maomaotest02"
BUYER_MEMBER_ID="maomaotest03"
TRADE_NO="000002342342"
TRADE_TYPE="alipay001"
MEMBER_FREEZE_MONEY="344"
ALI_FREEZE_MONEY="34"
GMT_AGREED_UNFREEZE="2010-01-28"
GMT_ACTUAL_UNFREEZE="2010-01-20"
GNT_FUND_STATUS="FREE"
GMT_CREATE="2010-01-25"
GMT_MODIFIED="2010-01-25"
IS_FREEZE_CREDIT="Y"
SOURCE_DETAIL="234234234"
/>
</dataset>
关键部分代码.
解释:
1、 setUpDataSet("dbunit/acFreezeSet.xml"
); --数据准备,该方法是dbunit 根据xml文件构造 IDataSet到内存中,表名为"CREDIT_GNT_AC_FREEZE"
,数据为上面xml文件数据。
数据源是通过 this
.applicationContext.getBean("dataSource"
);///关键
2、 acFreezeDAO通过spring容器注入。
acFreezeDO = this
.acFreezeDAO.find(2L);
查找id为2的数据,在数据准备阶段已经把数据放到内存中,正常情况下是可以查出来的。
代码跟到 "acFreezeDO = this.acFreezeDAO.find(2L);"这行代码,返回的DO为null,注:在IDE中单个单元测试是可以通过的。如图:
数据准备已经把ID为2 的值放到内存中了为什么查不出来呢?
debug 发现acFreezeDAO依赖的数据源与dbUnit基类DbUnitSpringTransactionalTestCase所依赖注入的数据源不一致,为什么呢,看图比较:
acFreezeDAO数据源 依赖 关系图:
dataSource类型为:BasicDataSource,applicationContext.xml文件中配置的类型为 SchemaAwareDataSourceProxy,这个怎么回事。。。
DbUnitSpringTransactionalTestCase.dataSource 注入的数据源如图:
dataSource类型为:SchemaAwareDataSourceProxy 与applicationContext.xml文件中配置的类型相同,这个是正确的。
疑问,同一个配置文件的数据源为什么取到类型截然不同呢???先保留疑问后面回答。
GroupCreateServiceTest的代码如下:
public
class GroupCreateServiceTest extends
BaseTestCase {
@SpringBeanByName
GroupCreateService groupCreateService;
@SpringBeanByName
private
AcFreezeDAO acFreezeDAO;
@Test
public
void test() {
////////////////代码省略
}
}
BaseTestCase 类代码:
@SpringApplicationContext( { "applicationContext.xml"
})
public
class BaseTestCase extends
JTester {
public
BaseTestCase(){
// 初始化log4j
URL url = BaseTestCase.class.getClassLoader().getResource("log4j_test.xml"
);
if
(url != null
) {
DOMConfigurator.configure(url);
} else
{
System
.err.println("not found log4jTest.xml in classpath"
);
}
}
}
spring配置文件与dbunit公用相同的文件。
jtester.properties 文件内容
#database.type=h2db
database.only.testdb.allowing=false
database.type=oracle
database.url=jdbc:oracle:thin:@10.20.36.18:1521:ocndb
database.userName=alibaba_ut
database.password=ca
database.schemaNames=ALIBABA_UT
database.driverClassName=com.alibaba.china.jdbc.SimpleDriver
DatabaseModule.Transactional.value.default
=rollback
database.driverJar=/home/liulin/.m2/repository/com/alibaba/shared/headquarters.jdbc.proxy/1.1/headquarters.jdbc.proxy-1.1.jar
dataSource.wrapInTransactionalProxy=false
debug 查看 acFreezeDAO所对应的数据源类型,如下图:
dataSource类型: BasicDataSource,怪了。。。冷静想一下Jtester也自已维护也有一套配置在jtester.properties中他是不是把applicationContext.xml中的dataSource替换掉了呢 ,带着这个疑问继续看Jtester的运行机制。
debug,Jtester启动代码发现,在加载Spring bean时,如果发现bean的id为dataSource 则默认用jtester内部购造一个数据源替换掉applicationContext.xml配置的数据源。看图:
类名为:org.jtester.unitils.spring.JTesterClassPathXmlApplicationContext ,有兴趣的可以研究一下。
注:jtester默认会去找bean id 为dataSource的数据源,还可以通过jtester.properties来指定,属性如下:
spring.datasource.name=jtesterDataSource
有个疑问,在运行AcFreezeDAOTest用例时,属性 acFreezeDAO 对应的数据源类型不是 Spring配置中配置的类型,是不是和jtester有关系 ,看如下代码:
package
org.jtester.unitils.database.util;
public
class JTesterDataSourceFactory implements
DataSourceFactory {
public
DataSource createDataSource() {
this
.checkDoesTestDB();
BasicDataSource dataSource = new
BasicDataSource();
this
.initFactualDataSource(dataSource);
this
.doesDisableDataSource(dataSource);
return
TracerUnitils.tracerDataSource(dataSource);
}
protected
void initFactualDataSource(BasicDataSource dataSource) {
log.info("Creating data source. Driver: "
+ type.getDriveClass() + ", url: "
+ type.getConnUrl() + ", user: "
+ type.getUserName() + ", password: <not shown>"
);
dataSource.setDriverClassName(type.getDriveClass());
dataSource.setUsername(type.getUserName());
dataSource.setPassword(type.getUserPass());
dataSource.setUrl(type.getConnUrl());
}
}
以上代码是jtester用来替换applicationContext.xml文件中配置的数据源。疑问解开了,是jtester做的怪。又有个疑问,在执行AcFreezeDAOTest用例时spring会创建一个新的上下文为什么 acFreezeDAO对应的数据源没有变化???
疑问定位到smartIbatis 框架上,是不是他做了处理?顺着dao的逻辑,数据源是sessionManager来管理,sessionManager由SessionFactoryManager来创建,看如下代码:
public
class IBatisSessionFactoryManager implements
SessionFactoryManager, InitializingBean {
private
static
final
Logger logger = LoggerFactory.getLogger(IBatisSessionFactoryManager.class);
/**
* 所有DataObject到sqlMapClient对象的反向映射缓存(cache),,为实现快速数据垂直路由。
*/
private
static
Map<String
, String
> dataObject2SqlMapClientMappingCache = new
HashMap<String
, String
>();
private
static
Map<String
, SessionManager> dataSource2SessionManagerCache = new
HashMap<String
, SessionManager>();
private
static
Map<String
, TransactionTemplate> dataObject2TransactionTemplateMappingCache = new
HashMap<String
, TransactionTemplate>();
/**
* dataObject 到 sqlMapClient 映射关系
*/
protected
void initDataObject2SqlMapClientMappingCache() {
// 已经初始化过,直接找到数据库标记
synchronized
(dataObject2SqlMapClientMappingCache) {
/////////////////////////////////////////////////////关键部分
////第一次初始化 IBatisSessionFactoryManager时会初始化sqlMapClientMapping,以后不管启动多少个Spring上下文都不会初始化,
////因为dataObject2SqlMapClientMappingCache是静态的在jvm级别共享数据。
if
(dataObject2SqlMapClientMappingCache.size() <= 0) {
initdataObject2SqlMapClientMapping();
}
}
}
/**
* 初始化 数据类名到 数据源反向映射,以便之后进行快速数据路由。
*/
@SuppressWarnings("unchecked"
)
protected
void initdataObject2SqlMapClientMapping() {
///部分代码省略
////// 初始化sqlMap
dataObject2SqlMapClientMappingCache.put(dataObjectClassName, dataSourceKey);
dataObject2TransactionTemplateMappingCache.put(dataObjectClassName, transactionTemplate);
}
/**
* 根据数据源标识和数据对象名获取对应的 SessionManager,对于IBatis,它封装了对应的SqlMapClient对象。
*
* @param dataSourceFlag
* @param dataObjectClassName
*/
public
SessionManager getSessionManager(String
dataObjectClassName) {
// 水平扩展要支持的话,这里返回的是一个 datasourceKey 数组。
String
datasourceKey = dataObject2SqlMapClientMappingCache.get(dataObjectClassName);
TransactionTemplate transactionTemplate = getTransactionTemplate(dataObjectClassName);
// datasourceKey gotten, get SessionManager from cache
SessionManager sessionManager = dataSource2SessionManagerCache.get(datasourceKey);
if
(sessionManager == null
) {
SqlMapClient sessionFactory = null
;
sessionFactory = (SqlMapClient) datasources.get(datasourceKey);
if
(sessionFactory == null
) {
throw
new
DaoException("SessionFactory instance ( Datasource) not found for
datasourceKey '"
+ datasourceKey + "' of dataObjectClass:"
+ dataObjectClassName);
}
// cache sessionManager
/////省略代码
// put in cache
////缓存sessionManager对象。
dataSource2SessionManagerCache.put(datasourceKey, sessionManager1);
if
(logger.isInfoEnabled()) logger.info("SessionManager created! DatasourceKey:{} = SessionManager:{},"
,
datasourceKey, sessionManager1);
// get from cache
sessionManager = dataSource2SessionManagerCache.get(datasourceKey);
}
return
sessionManager;
}
}
代码解释:
1、静态Map变量
dataObject2SqlMapClientMappingCache 缓存dataSourceKey,key为dataObjectClassName
dataSource2SessionManagerCache 缓存sessionManager对象,key为datasourceKey
dataObject2TransactionTemplateMappingCache 缓存transactionTemplate,key为dataObjectClassName
cache是static的,所以在jvm级别是共享的。
2、initDataObject2SqlMapClientMappingCache()方法
初始化 IBatisSessionFactoryManager 类会缓存数据类名到数据源反向映射
3、 getSessionManager()方法
缓存sessionManager,下次调用直接走cache.
问题的源头找到了,是cache做的怪。。。。
解决方法1:
重写initDataObject2SqlMapClientMappingCache方法,将缓存改成针对Spring上下文级别而不是jvm级别,代码如下:
public
class IBatisSessionFactoryManagerForTest extends
IBatisSessionFactoryManager {
private
static
final
Logger logger = LoggerFactory.getLogger(IBatisSessionFactoryManagerForTest.class);
/**
* dataObject 到 sqlMapClient 映射关系
*/
protected
void initDataObject2SqlMapClientMappingCache() {
try
{
/////////清空缓存
Field dataObject2SqlMapClientMappingCache = IBatisSessionFactoryManager.class.getDeclaredField("dataObject2SqlMapClientMappingCache"
);
dataObject2SqlMapClientMappingCache.setAccessible(true
);
dataObject2SqlMapClientMappingCache.set(dataObject2SqlMapClientMappingCache, new
HashMap<String
, String
>());
Field dataSource2SessionManagerCache = IBatisSessionFactoryManager.class.getDeclaredField("dataSource2SessionManagerCache"
);
dataSource2SessionManagerCache.setAccessible(true
);
dataSource2SessionManagerCache.set(dataSource2SessionManagerCache, new
HashMap<String
, String
>());
} catch
(SecurityException e) {
logger.error("", e);
} catch
(NoSuchFieldException e) {
logger.error("", e);
} catch
(IllegalArgumentException e) {
logger.error("", e);
} catch
(IllegalAccessException e) {
logger.error("", e);
}
/////重新加载sqlmap
initdataObject2SqlMapClientMapping();
}
}
注:此方法只能做为临时解决方案。
解决方法2:
升级apollo.smart-ibatis-1.0.1,将缓存的级别调整为Spring上下文级别。
与何坤讨论升级smart-ibatis框架,技术需求已经提了。。。
终于看到BUILD SUCCESSFUL了。。。
相关文章:
http://blog.csdn.net/liulin_good/archive/2011/02/21/6198544.aspx
http://wenku.baidu.com/view/dfe98feb6294dd88d0d26b3f.html
maven surefire插件源码:
http://svn.apache.org/repos/asf/maven/surefire/trunk/