持續整合之路——資料訪問層單元測試遇到的問題
在編寫資料訪問層的單元測試時,遇到不少問題,有些問題可以很容易Google到解決方法,而有些只能自己研究解決。這裡分享幾個典型的問題以及解決方法。
先交代一下用到的測試框架 Spring Test + SpringTestDbUnit + DbUnit。
一、先說一個低階的問題。
Spring通過<jdbc:embedded-database>標籤提供對記憶體資料的支援,形如:
<jdbc:embeded-database id="dataSource" type="HSQL">
可是在啟動時,卻總是提示錯誤:
Caused by: org.xml.sax.SAXParseException; lineNumber: 31; columnNumber: 57; cvc-complex-type.2.4.c: 萬用字元的匹配很全面, 但無法找到元素 'jdbc:embedded-database' 的宣告。
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:198)
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.error(ErrorHandlerWrapper.java:134)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:437)
翻來覆去對標籤修改了很多次,文件和dtd也看了很多遍,始終沒有發現問題。最後無意間看到context檔案頭部對標籤的宣告上好像有問題:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:task="http://www.springframework.org/schema/task" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.2.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/tx /spring-jdbc-3.2.xsd">
仔細看了下,原來當時從tx處複製宣告時,只是將最後的tx改成了jdbc,卻忘記了將路徑中tx改為jdbc。更改後,啟動正常。所有,如果有同學遇到類似的問題,應該先檢查頭部。
二、外來鍵關聯導致的刪除失敗。
在剛開始寫測試時,每個用例單獨執行都沒有問題,可是一旦一起執行,就出現下面的異常:
Tests run: 5, Failures: 0, Errors: 3, Skipped: 0, Time elapsed: 0.879 sec <<< FAILURE! - in com.noyaxe.nso.service.DeviceServiceTest
testInitializedForBindedSpaceForceBind(com.noyaxe.nso.service.DeviceServiceTest) Time elapsed: 0.309 sec <<< ERROR!
java.sql.SQLIntegrityConstraintViolationException: integrity constraint violation: foreign key no action; FK_L6IDVK78B2TLU8NO6EDJ0G6U8 table: CUSTOM_TABLE_COLUMN_SPACE_TYPE
at org.hsqldb.jdbc.Util.sqlException(Unknown Source)
at org.hsqldb.jdbc.Util.sqlException(Unknown Source)
at org.hsqldb.jdbc.JDBCStatement.fetchResult(Unknown Source)
……
……
Caused by: org.hsqldb.HsqlException: integrity constraint violation: foreign key no action; FK_L6IDVK78B2TLU8NO6EDJ0G6U8 table: CUSTOM_TABLE_COLUMN_SPACE_TYPE
at org.hsqldb.error.Error.error(Unknown Source)
at org.hsqldb.StatementDML.performReferentialActions(Unknown Source)
at org.hsqldb.StatementDML.delete(Unknown Source)
at org.hsqldb.StatementDML.executeDeleteStatement(Unknown Source)
at org.hsqldb.StatementDML.getResult(Unknown Source)
at org.hsqldb.StatementDMQL.execute(Unknown Source)
at org.hsqldb.Session.executeCompiledStatement(Unknown Source)
at org.hsqldb.Session.executeDirectStatement(Unknown Source)
at org.hsqldb.Session.execute(Unknown Source)
at org.hsqldb.jdbc.JDBCStatement.fetchResult(Unknown Source)
……
看異常資訊,應該是刪除記錄時,外來鍵級聯導致的問題。在實體類裡改變級聯設定並不起作用。最後在StackOverflow上找了一個解決方法:編寫一個類,繼承AbstractTestExecutionListener,在beforeTestClass中取消級聯依賴。具體如下:import org.dbunit.database.DatabaseDataSourceConnection;
import org.dbunit.database.IDatabaseConnection;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import javax.sql.DataSource;
public class ForeignKeyDisabling extends AbstractTestExecutionListener {
@Override
public void beforeTestClass(TestContext testContext) throws Exception {
IDatabaseConnection dbConn = new DatabaseDataSourceConnection(
testContext.getApplicationContext().getBean(DataSource.class)
);
dbConn.getConnection().prepareStatement("SET DATABASE REFERENTIAL INTEGRITY FALSE").execute();
}
}
把這個新的Listener新增測試類的註解中:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-test.xml")
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionDbUnitTestExecutionListener.class,
ForeignKeyDisabling.class})
參考:http://stackoverflow.com/questions/2685274/tdd-with-hsqldb-removing-foreign-keys
三、PROPERTY_DATATYPE_FACTORY引起的警告
在jenkins中構建時,總是可以看到如下的警告資訊:
WARN getDataTypeFactory, Potential problem found: The configured data type factory 'class org.dbunit.dataset.datatype.DefaultDataTypeFactory' might cause problems with the current database 'HSQL Database Engine' (e.g. some datatypes may not be supported properly). In rare cases you might see this message because the list of supported database products is incomplete (list=[derby]). If so please request a java-class update via the forums.If you are using your own IDataTypeFactory extending DefaultDataTypeFactory, ensure that you override getValidDbProducts() to specify the supported database products.
意思很好理解,就說預設的DataTypeFactory可能會引起問題,建議設定該屬性值。解決方法也很明顯:就是設定資料庫連線的PROPERTY_DATATYPE_FACTORY屬性的值。嘗試了用Before、BeforeClass或者自定義ExecutionListener中都無法實現對該屬性的設定。那就只能先找到丟擲這個異常的位置,然後向前推,逐步找到獲取連線的地方。最後發現,連線是在DbUnitTestExecutionListener.prepareDatabaseConnection中獲取連線,並且沒有做什麼進一步的處理,所以前面的設定都不起作用。看來又只能通過重寫原始碼來達成目的了。
直接上原始碼吧:
CustomTransactionDbUnitTestExecutionListener類: 完全複製DbUnitTestExecutionListener,只是增加一句程式碼。注意該類的包路徑和DbUnitTestExecutionListener一致。
private void prepareDatabaseConnection(TestContext testContext, String databaseConnectionBeanName) throws Exception {
Object databaseConnection = testContext.getApplicationContext().getBean(databaseConnectionBeanName);
if (databaseConnection instanceof DataSource) {
databaseConnection = DatabaseDataSourceConnectionFactoryBean.newConnection((DataSource) databaseConnection);
}
Assert.isInstanceOf(IDatabaseConnection.class, databaseConnection);
((IDatabaseConnection)databaseConnection).getConfig().setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new HsqldbDataTypeFactory());
testContext.setAttribute(CONNECTION_ATTRIBUTE, databaseConnection);
}
綠色就是真正發揮作用的程式碼。
可是這個類並不能直接飲用,而是通過TransactionDbUnitTestExecutionListener的CHAIN被呼叫的,而TransactionDbUnitTestExecutionListener同樣無法更改,同樣只能建一個自定義的TransactionDbUnitTestExecutionListener類,CustomTransactionDbUnitTestExecutionListener:
public class CustomTransactionDbUnitTestExecutionListener extends TestExecutionListenerChain {
private static final Class<?>[] CHAIN = { TransactionalTestExecutionListener.class,
CustomDbUnitTestExecutionListener.class };
@Override
protected Class<?>[] getChain() {
return CHAIN;
}
}
那麼測試類的註解也要修改:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-test.xml")
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
CustomTransactionDbUnitTestExecutionListener.class,
ForeignKeyDisabling.class})
四、@Transactional標籤引起的問題
按照spring-dbunit-test的文件中說法,可以使用@Transactional確保資料的清潔。使用簡單,只需要將上面的註解增加一個@Transactional,
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-test.xml")
@Transactional
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
CustomTransactionDbUnitTestExecutionListener.class,
ForeignKeyDisabling.class})
可是執行時,卻出現了異常:
org.springframework.transaction.TransactionSystemException: Could not roll back JPA transaction; nested exception is javax.persistence.PersistenceException: unexpected error when rollbacking
at org.springframework.orm.jpa.JpaTransactionManager.doRollback(JpaTransactionManager.java:544)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:846)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.rollback(AbstractPlatformTransactionManager.java:823)
at org.springframework.test.context.transaction.TransactionalTestExecutionListener$TransactionContext.endTransaction(TransactionalTestExecutionListener.java:588)
at org.springframework.test.context.transaction.TransactionalTestExecutionListener.endTransaction(TransactionalTestExecutionListener.java:297)
at org.springframework.test.context.transaction.TransactionalTestExecutionListener.afterTestMethod(TransactionalTestExecutionListener.java:192)
……
Caused by: javax.persistence.PersistenceException: unexpected error when rollbacking
at org.hibernate.ejb.TransactionImpl.rollback(TransactionImpl.java:109)
at org.springframework.orm.jpa.JpaTransactionManager.doRollback(JpaTransactionManager.java:540)
... 32 more
Caused by: org.hibernate.TransactionException: rollback failed
at org.hibernate.engine.transaction.spi.AbstractTransactionImpl.rollback(AbstractTransactionImpl.java:215)
at org.hibernate.ejb.TransactionImpl.rollback(TransactionImpl.java:106)
... 33 more
Caused by: org.hibernate.TransactionException: unable to rollback against JDBC connection
at org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction.doRollback(JdbcTransaction.java:167)
at org.hibernate.engine.transaction.spi.AbstractTransactionImpl.rollback(AbstractTransactionImpl.java:209)
... 34 more
Caused by: java.sql.SQLNonTransientConnectionException: connection exception: connection does not exist
at org.hsqldb.jdbc.Util.sqlException(Unknown Source)
at org.hsqldb.jdbc.Util.sqlException(Unknown Source)
……
... 35 more
Caused by: org.hsqldb.HsqlException: connection exception: connection does not exist
at org.hsqldb.error.Error.error(Unknown Source)
at org.hsqldb.error.Error.error(Unknown Source)
... 40 more
最後通過檢視原始碼發現,CustomDbUnitTestExecutionListener會先於TransactionalTestExecutionListener執行,而前者在執行完畢就關閉了資料庫連線,後者在回滾時,就發生了連線不存在的異常。
解決方法很簡單,修改CustomTransactionalDbUnitTestExecutionListener:
private static final Class<?>[] CHAIN = {CustomDbUnitTestExecutionListener.class, TransactionalTestExecutionListener.class};
也就是陣列兩個元素調換下位置。