數(shù)據(jù)庫(kù)測(cè)試
在創(chuàng)建企業(yè)級(jí)應(yīng)用的時(shí)候,數(shù)據(jù)層的單元測(cè)試因?yàn)槠鋸?fù)雜性往往被遺棄,Unitils大大降低了測(cè)試的復(fù)雜性,使得數(shù)據(jù)庫(kù)的測(cè)試變得容易并且易維護(hù)。已下介紹databasemodule和dbunitmodule進(jìn)行數(shù)據(jù)庫(kù)的單元測(cè)試。
用dbUnit管理測(cè)試數(shù)據(jù)
數(shù)據(jù)庫(kù)的測(cè)試應(yīng)該在單元測(cè)試數(shù)據(jù)庫(kù)上運(yùn)行,單元測(cè)試數(shù)據(jù)庫(kù)給我們提供了一個(gè)完整的并有著很好細(xì)粒度控制的測(cè)試數(shù)據(jù),DbUnitModule是在dbunit的基礎(chǔ)上進(jìn)一步的為數(shù)據(jù)庫(kù)的測(cè)試提供數(shù)據(jù)集的支持。
加載測(cè)試數(shù)據(jù)集
讓我們以UserDAO中一個(gè)簡(jiǎn)單的方法findByName(檢查姓氏和名字)為例子開(kāi)始介紹。他的單元測(cè)試如下:
@DataSet
public class UserDAOTest extends UnitilsJUnit4 {
@Test
public void testFindByName() {
User result = userDao.findByName("doe", "john");
assertPropertyLenEquals("userName", "jdoe", result);
}
@Test
public void testFindByMinimalAge() {
List<User> result = userDao.findByMinimalAge(18);
assertPropertyLenEquals("firstName", Arrays.asList("jack"), result);
}
}
@DateSet 注解表示了測(cè)試需要尋找dbunit的數(shù)據(jù)集文件進(jìn)行加載,如果沒(méi)有指明數(shù)據(jù)集的文件名,則Unitils自動(dòng)在class文件的同目錄下加載文件名為 className.xml的數(shù)據(jù)集文件。(這種定義到class上面的數(shù)據(jù)集稱為class級(jí)別的數(shù)據(jù)集)
數(shù)據(jù)集 文件必須是dbunit的FlatXMLDataSet文件格式,其中包含了所要測(cè)試的數(shù)據(jù)。測(cè)試數(shù)據(jù)庫(kù)表中所有的內(nèi)容將會(huì)被刪除,然后再插入數(shù)據(jù)集中的 數(shù)據(jù)。如果表不屬于數(shù)據(jù)集中的,哪么該表的數(shù)據(jù)將不會(huì)被刪除。你也可以明確的加入一個(gè)空的表元素,例如<MY_TABLE/>(可以達(dá)到刪除 測(cè)試數(shù)據(jù)庫(kù)表中內(nèi)容的作用),如果要明確指定一個(gè)空的值,那么使用值[null]。
為UserDAOTest我們創(chuàng)建一個(gè)數(shù)據(jù)集,并放在UserDAOTest.class文件同目錄下。
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<usergroup name="admin" />
<user userName="jdoe" name="doe" firstname="john" userGroup="admin" />
<usergroup name="sales" />
<user userName="smith" name="smith" userGroup="sales" />
</dataset>
測(cè)試運(yùn)行的時(shí)候,首先將刪除掉usergroup表和user表中的所有內(nèi)容,然后將插入數(shù)據(jù)集中的內(nèi)容。其中name為smith的firstname的值將會(huì)是null。
假設(shè)testFindByMinimalAge()方法將使用一個(gè)特殊的數(shù)據(jù)集而不是使用class級(jí)別的數(shù)據(jù)集,你可以定義一個(gè)UserDAOTest.testFindByMinimalAge.xml 數(shù)據(jù)集文件并放在測(cè)試類的class文件同目錄下。
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<user userName="jack" age="18" />
<user userName="jim" age="17" />
</dataset>
這時(shí),你在testFindByMinimalAge()方法使用@DataSet注解,他將覆蓋class級(jí)的數(shù)據(jù)集
public class UserDAOTest extends UnitilsJUnit4 {
@Test
@DataSet("UserDAOTest.testFindByMinimalAge.xml")
public void testFindByMinimalAge() {
List<User> result = userDao.findByMinimalAge(18);
assertPropertyLenEquals("firstName", Arrays.asList("jack"), result);
}
}
不要過(guò)多的使用method級(jí)的數(shù)據(jù)集,因?yàn)檫^(guò)多的數(shù)據(jù)集文件意味著你要花大量的時(shí)間去維護(hù),你優(yōu)先考慮的是使用class級(jí)的數(shù)據(jù)集。
配置數(shù)據(jù)集加載策略
缺省情況下數(shù)據(jù)集被寫(xiě)入數(shù)據(jù)庫(kù)采用的是clean insert策略。這就意味著數(shù)據(jù)在被寫(xiě)入數(shù)據(jù)庫(kù)的時(shí)候是會(huì)先刪除數(shù)據(jù)集中有使用的表的數(shù)據(jù),然后在將數(shù)據(jù)集中的數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù)。加載策略是可配額制的,我們通過(guò)修改DbUnitModule.DataSet.loadStrategy.default 的屬性值來(lái)改變加載策略。假設(shè)我們?cè)趗nitils.properties屬性文件中加入以下內(nèi)容:
DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.InsertLoadStrategy
這時(shí)加載策略就由clean insert變成了insert,數(shù)據(jù)已經(jīng)存在表中將不會(huì)被刪除,測(cè)試數(shù)據(jù)只是進(jìn)行插入操作。
加載策略也可以使用@DataSet的注解屬性對(duì)單獨(dú)的一些測(cè)試進(jìn)行配置:
@DataSet(loadStrategy = InsertLoadStrategy.class)
對(duì)于那些樹(shù)形DbUnit的人來(lái)說(shuō),配置加載策略實(shí)際上就是使用不同的DatabaseOperation,以下是默認(rèn)支持的加載策略方式:
l CleanInsertLoadStrategy: 先刪除dateSet中有關(guān)表的數(shù)據(jù),然后再插入數(shù)據(jù)。
l InsertLoadStrategy: 只插入數(shù)據(jù)。
l RefreshLoadStrategy: 有同樣key的數(shù)據(jù)更新,沒(méi)有的插入。
l UpdateLoadStrategy: 有同樣key的數(shù)據(jù)更新,沒(méi)有的不做任何操作。
配置數(shù)據(jù)集工廠
在Unitils中數(shù)據(jù)集文件采用了multischema xml 格式,這是DbUnits的FlatXmlDataSet 格式的擴(kuò)展。配置文件格式和文件的擴(kuò)展可以采用DataSetFactory 。
雖然Unitils當(dāng)前只支持一種數(shù)據(jù)格式,但是我們可以通過(guò)實(shí)現(xiàn)DataSetFactory來(lái)使用其他文件格式。當(dāng)你想使用excel而不是xml格式的時(shí)候,可以通過(guò)unitils.property中的DbUnitModule.DataSet.factory.default 屬性和@DataSet 注解來(lái)創(chuàng)建一個(gè)DbUnit's XlsDataSet 實(shí)例。
驗(yàn)證測(cè)試結(jié)果
有些時(shí)候我們想在測(cè)試時(shí)完畢后使用數(shù)據(jù)集來(lái)檢查數(shù)據(jù)庫(kù)中的內(nèi)容,舉個(gè)例子當(dāng)執(zhí)行完畢一個(gè)存儲(chǔ)過(guò)程后你想檢查一下啊數(shù)據(jù)是否更新了沒(méi)有。
下面的例子表示的是禁用到一年內(nèi)沒(méi)有使用過(guò)的帳戶
public class UserDAOTest extends UnitilsJUnit4 {
@Test @ExpectedDataSet
public void testInactivateOldAccounts() {
userDao.inactivateOldAccounts();
}
}
注意在test方法上增加了一個(gè)@ExpectedDataSet 注解。這將指明unitils將使用UserDAOTest.testInactivateOldAccounts-result.xml 這個(gè)數(shù)據(jù)集的內(nèi)容和數(shù)據(jù)庫(kù)的內(nèi)容進(jìn)行比較。
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<user userName="jack" active="true" />
<user userName="jim" active="false" />
</dataset>
根據(jù)這個(gè)數(shù)據(jù)集,將會(huì)檢查是否有兩條和記錄集的值相同的記錄在數(shù)據(jù)庫(kù)中。而其他的記錄和表將不理會(huì)。
使用的是@DataSet 注解的話,文件名可以明確指出,如果文件名沒(méi)有明確指出來(lái),那么文件名將匹配className .methodName -result.xml
使用少使用結(jié)果數(shù)據(jù)集,加入新的數(shù)據(jù)集意味著更多的維護(hù)。替代方式是在代碼中執(zhí)行相同的檢查(如使用一個(gè)findactiveusers()方法)。
使用多模式的數(shù)據(jù)集
一個(gè)程序不單單只是連接一個(gè)數(shù)據(jù)庫(kù)shema。Unitils采用了擴(kuò)展的數(shù)據(jù)集xml來(lái)定義多schemas下的數(shù)據(jù)。以下就是一個(gè)讀取數(shù)據(jù)到2個(gè)不同的schemas中的例子:
<?xml version='1.0' encoding='UTF-8'?>
<dataset xmlns="SCHEMA_A" xmlns:b="SCHEMA_B">
<user id="1" userName="jack" />
<b:role id="1" roleName="admin" />
</dataset>
在這個(gè)例子中我定義了兩個(gè)schemas,SCHEMA_A 和 SCHEMA_B。第一個(gè)schema,SCHEMA_A 被連接到默認(rèn)的xml命名空間中,第二個(gè)schema,SCHEMA_B 被連接到命名空間b。如果表xml元素的前綴使用了命名空間b,那么該表就是schema SCHEMA_B 中的,如果沒(méi)有使用任何的命名空間那么該表將被認(rèn)為是SCHEMA_A
中的。以上例子中測(cè)試數(shù)據(jù)定義了表SCHEMA_A.user 和SCHEMA_B.role。
如果在數(shù)據(jù)集中沒(méi)有配置一個(gè)默認(rèn)的命名空間,那么將會(huì)采用在unitils.properties中的屬性database.schemaNames 的第一個(gè)值作為默認(rèn)的
database.schemaNames=SCHEMA_A, SCHEMA_B
這個(gè)配置將SCHEMA_A 作為缺省的schema,這樣你可以簡(jiǎn)化數(shù)據(jù)集的聲明。
<?xml version='1.0' encoding='UTF-8'?>
<dataset xmlns:b="SCHEMA_B">
<user id="1" userName="jack" />
<b:role id="1" roleName="admin" />
</dataset>
連接測(cè)試數(shù)據(jù)庫(kù)
在以上所有的例子中,我們都有一件重要的事情沒(méi)有做:當(dāng)我們進(jìn)行測(cè)試的時(shí)候,怎樣連接數(shù)據(jù)庫(kù)并得到DataSource?
當(dāng)測(cè)試套件的第一個(gè)測(cè)試數(shù)據(jù)庫(kù)的案例運(yùn)行的時(shí)候,Unitils將會(huì)通過(guò)屬性文件創(chuàng)建一個(gè)DataSource 的實(shí)例來(lái)連接你單元測(cè)試時(shí)的數(shù)據(jù)庫(kù),以后的測(cè)試中都將使用這個(gè)DataSource 實(shí)例。連接配置的詳細(xì)內(nèi)容如下:
database.driverClassName=oracle.jdbc.driver.OracleDriver
database.url=jdbc:oracle:thin:@yourmachine:1521:YOUR_DB
database.userName=john
database.password=secret
database.schemaNames=test_john
按配置章節(jié)所說(shuō)的那樣,你可以將連接數(shù)據(jù)庫(kù)的驅(qū)動(dòng)類和url地址配置到unitils.properties 中去,而用戶名,密碼以及schema可以配置到unitils-local.properties 中去,這樣可以讓開(kāi)發(fā)人員連接到自己的單元測(cè)試數(shù)據(jù)庫(kù)中進(jìn)行測(cè)試而不會(huì)干預(yù)到其他的人。
在屬性或者setter方法前使用注解@TestDataSource ,將會(huì)將DataSource 實(shí)例注入到測(cè)試實(shí)例中去,如果你想加入一些代碼或者配置一下你的datasource,你可以做一個(gè)抽象類來(lái)實(shí)現(xiàn)該功能,所有的測(cè)試類都繼承該類。一個(gè)簡(jiǎn)單的例子如下:
public abstract class BaseDAOTest extends UnitilsJUnit4 {
@TestDataSource
private DataSource dataSource;
@Before
public void initializeDao() {
BaseDAO dao = getDaoUnderTest();
dao.setDataSource(dataSource);
}
protected abstract BaseDAO getDaoUnderTest();
}
上面的例子采用了注解來(lái)取得一個(gè)datasource的引用,另外一種方式就是使用DatabaseUnitils.getDataSource() 方法來(lái)取得datasource。
事務(wù)
出于不同的原因,我們的測(cè)試都是運(yùn)行在一個(gè)事務(wù)中的,其中最重要的原因如下:
l 數(shù)據(jù)庫(kù)的很多action都是在事務(wù)正常提交后才做,如SELECT FOR UPDATE 和觸發(fā)器
l 許多項(xiàng)目在測(cè)試數(shù)據(jù)的時(shí)候都會(huì)填寫(xiě)一些測(cè)試數(shù)據(jù),每個(gè)測(cè)試運(yùn)行都會(huì)修改或者更新了數(shù)據(jù),當(dāng)下一個(gè)測(cè)試運(yùn)行的時(shí)候,都需要將數(shù)據(jù)回復(fù)到原有的狀態(tài)。
l 如果使用的是hibernate或者JPA的時(shí)候,都需要每個(gè)測(cè)試都運(yùn)行在事務(wù)中,保證系統(tǒng)的正常工作。
缺省情況下,事務(wù)管理是disabled的,事務(wù)的默認(rèn)行為我們可以通過(guò)屬性文件的配置加以改變:
DatabaseModule.Transactional.value.default=commit
采用這個(gè)設(shè)置,每個(gè)的測(cè)試都將執(zhí)行commit,其他的屬性值還有rollback 和disabled
我們也可以通過(guò)在測(cè)試類上使用注解@Transactional 來(lái)改變默認(rèn)的事務(wù)設(shè)置,如:
@Transactional(TransactionMode.ROLLBACK)
public class UserDaoTest extends UnitilsJUnit4 {
通過(guò)這種class上注解的事務(wù)管理,可以讓每個(gè)測(cè)試都確保回滾,@Transactional 注解還可以繼承的,因此我們可以將其放在父類中,而不必每個(gè)子類都進(jìn)行聲明。
.........
如果你使用Unitils的spring支持(見(jiàn)使用spring進(jìn)行測(cè)試)你如果配置了PlatformTransactionManager 的bean,那么unitils將會(huì)使用這個(gè)事務(wù)管理。