| |||||||||
日 | 一 | 二 | 三 | 四 | 五 | 六 | |||
---|---|---|---|---|---|---|---|---|---|
25 | 26 | 27 | 28 | 29 | 30 | 31 | |||
1 | 2 | 3 | 4 | 5 | 6 | 7 | |||
8 | 9 | 10 | 11 | 12 | 13 | 14 | |||
15 | 16 | 17 | 18 | 19 | 20 | 21 | |||
22 | 23 | 24 | 25 | 26 | 27 | 28 | |||
29 | 30 | 1 | 2 | 3 | 4 | 5 |
數據庫測試
在創建企業級應用的時候,數據層的單元測試因為其復雜性往往被遺棄,Unitils大大降低了測試的復雜性,使得數據庫的測試變得容易并且易維護。已下介紹databasemodule和dbunitmodule進行數據庫的單元測試。
用dbUnit管理測試數據
數據庫的測試應該在單元測試數據庫上運行,單元測試數據庫給我們提供了一個完整的并有著很好細粒度控制的測試數據,DbUnitModule是在dbunit的基礎上進一步的為數據庫的測試提供數據集的支持。
加載測試數據集
讓我們以UserDAO中一個簡單的方法findByName(檢查姓氏和名字)為例子開始介紹。他的單元測試如下:
@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 注解表示了測試需要尋找dbunit的數據集文件進行加載,如果沒有指明數據集的文件名,則Unitils自動在class文件的同目錄下加載文件名為 className.xml的數據集文件。(這種定義到class上面的數據集稱為class級別的數據集)
數據集 文件必須是dbunit的FlatXMLDataSet文件格式,其中包含了所要測試的數據。測試數據庫表中所有的內容將會被刪除,然后再插入數據集中的 數據。如果表不屬于數據集中的,哪么該表的數據將不會被刪除。你也可以明確的加入一個空的表元素,例如<MY_TABLE/>(可以達到刪除 測試數據庫表中內容的作用),如果要明確指定一個空的值,那么使用值[null]。
為UserDAOTest我們創建一個數據集,并放在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>
測試運行的時候,首先將刪除掉usergroup表和user表中的所有內容,然后將插入數據集中的內容。其中name為smith的firstname的值將會是null。
假設testFindByMinimalAge()方法將使用一個特殊的數據集而不是使用class級別的數據集,你可以定義一個UserDAOTest.testFindByMinimalAge.xml 數據集文件并放在測試類的class文件同目錄下。
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<user userName="jack" age="18" />
<user userName="jim" age="17" />
</dataset>
這時,你在testFindByMinimalAge()方法使用@DataSet注解,他將覆蓋class級的數據集
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);
}
}
不要過多的使用method級的數據集,因為過多的數據集文件意味著你要花大量的時間去維護,你優先考慮的是使用class級的數據集。
配置數據集加載策略
缺省情況下數據集被寫入數據庫采用的是clean insert策略。這就意味著數據在被寫入數據庫的時候是會先刪除數據集中有使用的表的數據,然后在將數據集中的數據寫入數據庫。加載策略是可配額制的,我們通過修改DbUnitModule.DataSet.loadStrategy.default 的屬性值來改變加載策略。假設我們在unitils.properties屬性文件中加入以下內容:
DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.InsertLoadStrategy
這時加載策略就由clean insert變成了insert,數據已經存在表中將不會被刪除,測試數據只是進行插入操作。
加載策略也可以使用@DataSet的注解屬性對單獨的一些測試進行配置:
@DataSet(loadStrategy = InsertLoadStrategy.class)
對于那些樹形DbUnit的人來說,配置加載策略實際上就是使用不同的DatabaseOperation,以下是默認支持的加載策略方式:
l CleanInsertLoadStrategy: 先刪除dateSet中有關表的數據,然后再插入數據。
l InsertLoadStrategy: 只插入數據。
l RefreshLoadStrategy: 有同樣key的數據更新,沒有的插入。
l UpdateLoadStrategy: 有同樣key的數據更新,沒有的不做任何操作。
配置數據集工廠
在Unitils中數據集文件采用了multischema xml 格式,這是DbUnits的FlatXmlDataSet 格式的擴展。配置文件格式和文件的擴展可以采用DataSetFactory 。
雖然Unitils當前只支持一種數據格式,但是我們可以通過實現DataSetFactory來使用其他文件格式。當你想使用excel而不是xml格式的時候,可以通過unitils.property中的DbUnitModule.DataSet.factory.default 屬性和@DataSet 注解來創建一個DbUnit's XlsDataSet 實例。
驗證測試結果
有些時候我們想在測試時完畢后使用數據集來檢查數據庫中的內容,舉個例子當執行完畢一個存儲過程后你想檢查一下啊數據是否更新了沒有。
下面的例子表示的是禁用到一年內沒有使用過的帳戶
public class UserDAOTest extends UnitilsJUnit4 {
@Test @ExpectedDataSet
public void testInactivateOldAccounts() {
userDao.inactivateOldAccounts();
}
}
注意在test方法上增加了一個@ExpectedDataSet 注解。這將指明unitils將使用UserDAOTest.testInactivateOldAccounts-result.xml 這個數據集的內容和數據庫的內容進行比較。
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<user userName="jack" active="true" />
<user userName="jim" active="false" />
</dataset>
根據這個數據集,將會檢查是否有兩條和記錄集的值相同的記錄在數據庫中。而其他的記錄和表將不理會。
使用的是@DataSet 注解的話,文件名可以明確指出,如果文件名沒有明確指出來,那么文件名將匹配className .methodName -result.xml
使用少使用結果數據集,加入新的數據集意味著更多的維護。替代方式是在代碼中執行相同的檢查(如使用一個findactiveusers()方法)。
使用多模式的數據集
一個程序不單單只是連接一個數據庫shema。Unitils采用了擴展的數據集xml來定義多schemas下的數據。以下就是一個讀取數據到2個不同的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>
在這個例子中我定義了兩個schemas,SCHEMA_A 和 SCHEMA_B。第一個schema,SCHEMA_A 被連接到默認的xml命名空間中,第二個schema,SCHEMA_B 被連接到命名空間b。如果表xml元素的前綴使用了命名空間b,那么該表就是schema SCHEMA_B 中的,如果沒有使用任何的命名空間那么該表將被認為是SCHEMA_A
中的。以上例子中測試數據定義了表SCHEMA_A.user 和SCHEMA_B.role。
如果在數據集中沒有配置一個默認的命名空間,那么將會采用在unitils.properties中的屬性database.schemaNames 的第一個值作為默認的
database.schemaNames=SCHEMA_A, SCHEMA_B
這個配置將SCHEMA_A 作為缺省的schema,這樣你可以簡化數據集的聲明。
<?xml version='1.0' encoding='UTF-8'?>
<dataset xmlns:b="SCHEMA_B">
<user id="1" userName="jack" />
<b:role id="1" roleName="admin" />
</dataset>
連接測試數據庫
在以上所有的例子中,我們都有一件重要的事情沒有做:當我們進行測試的時候,怎樣連接數據庫并得到DataSource?
當測試套件的第一個測試數據庫的案例運行的時候,Unitils將會通過屬性文件創建一個DataSource 的實例來連接你單元測試時的數據庫,以后的測試中都將使用這個DataSource 實例。連接配置的詳細內容如下:
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
按配置章節所說的那樣,你可以將連接數據庫的驅動類和url地址配置到unitils.properties 中去,而用戶名,密碼以及schema可以配置到unitils-local.properties 中去,這樣可以讓開發人員連接到自己的單元測試數據庫中進行測試而不會干預到其他的人。
在屬性或者setter方法前使用注解@TestDataSource ,將會將DataSource 實例注入到測試實例中去,如果你想加入一些代碼或者配置一下你的datasource,你可以做一個抽象類來實現該功能,所有的測試類都繼承該類。一個簡單的例子如下:
public abstract class BaseDAOTest extends UnitilsJUnit4 {
@TestDataSource
private DataSource dataSource;
@Before
public void initializeDao() {
BaseDAO dao = getDaoUnderTest();
dao.setDataSource(dataSource);
}
protected abstract BaseDAO getDaoUnderTest();
}
上面的例子采用了注解來取得一個datasource的引用,另外一種方式就是使用DatabaseUnitils.getDataSource() 方法來取得datasource。
事務
出于不同的原因,我們的測試都是運行在一個事務中的,其中最重要的原因如下:
l 數據庫的很多action都是在事務正常提交后才做,如SELECT FOR UPDATE 和觸發器
l 許多項目在測試數據的時候都會填寫一些測試數據,每個測試運行都會修改或者更新了數據,當下一個測試運行的時候,都需要將數據回復到原有的狀態。
l 如果使用的是hibernate或者JPA的時候,都需要每個測試都運行在事務中,保證系統的正常工作。
缺省情況下,事務管理是disabled的,事務的默認行為我們可以通過屬性文件的配置加以改變:
DatabaseModule.Transactional.value.default=commit
采用這個設置,每個的測試都將執行commit,其他的屬性值還有rollback 和disabled
我們也可以通過在測試類上使用注解@Transactional 來改變默認的事務設置,如:
@Transactional(TransactionMode.ROLLBACK)
public class UserDaoTest extends UnitilsJUnit4 {
通過這種class上注解的事務管理,可以讓每個測試都確保回滾,@Transactional 注解還可以繼承的,因此我們可以將其放在父類中,而不必每個子類都進行聲明。
.........
如果你使用Unitils的spring支持(見使用spring進行測試)你如果配置了PlatformTransactionManager 的bean,那么unitils將會使用這個事務管理。
1、使用Dir:配置
和大多數的項目一樣,unitils也需要一些配置,默認情況下有3個配置,每個配置都覆寫前一個的配置
第一個配置文件unitils-default.properties,它包含了缺省值并被包含在unitils的發行包中。我們沒有必要對這個文件進行修改,但它可以用來作參考。
第二個配置文件unitils.properties,它是我們需要進行配置的文件,并且能覆寫缺省的配置。舉個例子,如果你的項目使用的是oracle數據庫,你可以創建一個unitils.properties文件并覆寫相應的driver class和database url。
database.driverClassName=oracle.jdbc.driver.OracleDriver database.url=jdbc:oracle:thin:@yourmachine:1521:YOUR_DB
這個文件并不是必須的,但是一旦你創建了一個,你就需要將該文件放置在項目的classpath下
最后一個文件,unitils-local.properties是可選的配置文件,它可以覆寫項目的配置,用來定義開發者的具體設置,舉個例子來說,如果每個開發者都使用自己的數據庫schema,你就可以創建一個unitils-local.properties為每個用戶配置自己的數據庫賬號、密碼和schema。
database.userName=john database.password=secret database.schemaNames=test_john
每個unitils-local.properties文件應該放置在對應的用戶文件夾中(System.getProperty("user.home"))。
本地文件名unitils-local.properties也可以通過配置文件定義,在unitils.properties覆寫unitils.configuration.localFileName就可以。
unitils.configuration.localFileName=projectTwo-local.properties
啟用你的unitils
unitils的功能是依賴于基礎的測試框架,要使用unitils的功能,就必須先enable他們,這樣做的目的也是為了容易擴展。目前支持的框架有:
舉個例子,如果使用JUnit3,你要使用unitils
import org.unitils.UnitilsJUnit3; public class MyTest extends UnitilsJUnit3 { }
通常你將創建你自己的包含一些公共行為的測試基類,如dataSource的注入,你可以讓這個基類繼承unitils測試類。
當你使用的是JUnit4的話,你也可是使用@RunWith來代替繼承unitils測試類
import org.junit.runner.RunWith; import org.unitils.UnitilsJUnit4TestClassRunner; @RunWith(UnitilsJUnit4TestClassRunner.class) public class MyTest { }
模塊系統
在開始舉例之前,讓我們先了解一下unitils概念。
unitils的結構被設計成了十分容易擴展,每個模塊提供了一種服務,當執行Test的時候通過TestListener調用相應的服務。
這種設計采用了一個統一的方式提供服務,如果你需要加入其他的服務,無需去改編測試基類(UnitilsJUnit4這些類)。要加入新的服務只需要添加一個新的模塊并在unitls配置文件中登記這個模塊。
目前unitils中所有有效的模塊如下:
單元測試應該是簡單和直觀的,而現實中的項目大多都是采用多層方式的,如EJB和hibernate的數據驅動層的中間件技術。
unitils來源于一個嘗試,就是希望能以更務實的方式來看待單元測試......
這個指南會告訴你,什么項目可以使用unitils。 并在這個指導方針頁 中你可以了解到測試的準側和它的特點。如果您想了解如何可以配置unitils ,并得以迅速地啟動,請查看cookbook 。
unitils的斷言
在開始這個指南之前我們先說明一下獨立于unitils核心模塊的斷言。在下面的例子中,不需要進行配置,將unitils的jar包和依賴包放在你的classpath下,就可以進行測試了。
通過反射進行斷言
一個典型的單元測試包含了結果值和期望值的比較,unitils提供了斷言的方法以幫助你進行該操作,讓我們看看實例2中對有著id、first name、last name屬性的User類的2個實例的比較
public class User { private long id; private String first; private String last; public User(long id, String first, String last) { this.id = id; this.first = first; this.last = last; } } User user1 = new User(1, "John", "Doe"); User user2 = new User(1, "John", "Doe"); assertEquals(user1, user2);
你期望這個斷言是成功的,因為這兩個實例含有相同的屬性,但是運行的結果并非如此,應為User類并沒有覆寫
equals()方法,所以assertEquals是對兩個實例是否相等進行判斷(user1 == user2)結果導致了比較的失敗。
假設你像如下代碼一樣實現了equals方法
public boolean equals(Object object) { if (object instanceof User) { return id == ((User) object).id; } return false; }
這在你的程序邏輯中是一個合乎邏輯的實現,當兩個User實例擁有相同的id的時候,那么這兩個實例就是相等的。然而這種方式在你的單元測試中并不合適,并不能通過id的相同來認為兩個user是相同的。
User user1 = new User(1, "John", "Doe"); User user2 = new User(1, "Jane", "Smith"); assertEquals(user1, user2);
這個斷言將會成功,但這并不是你所期望的,因此不要使用assertEquals來對兩個對象進行判定是否相等(外覆類和java.lang.String類除外)。要想斷言他們相等,一種方法就是斷言每個屬性相等。
User user1 = new User(1, "John", "Doe"); User user2 = new User(1, "John", "Doe"); assertEquals(user1.getId(), user2.getId()); assertEquals(user1.getFirst(), user2.getFirst()); assertEquals(user1.getLast(), user2.getLast());
unitils提供了一些方法來幫助你執行斷言更加的簡單,通過反射,使用ReflectionAssert.assertRefEquals上面的代碼重寫如下:
User user1 = new User(1, "John", "Doe"); User user2 = new User(1, "John", "Doe"); assertRefEquals(user1, user2);
這個斷言將通過反射對兩個實例中的每個屬性都進行比較,先是id、然后是first name、最后是last name。
如果一個屬性本身也是一個對象,那么將會使用反射進行遞歸比較,這同樣適合與集合、map、和數組之間的比較,他們
的每個元素會通過反射進行比較。如果值是一個基本類型或者是一個外覆類,那么將會使用==進行值的比較,因此下面的斷
言會取得成功
assertRefEquals(1, 1L);
List<Double> myList = new ArrayList<Double>();
myList.add(1.0);
myList.add(2.0);
assertRefEquals(Arrays.asList(1, 2), myList);
寬松的斷言
出于可維護性,這一點是十分重要的,舉例說明:如果你要計算一個帳戶的余額,那你就沒比較檢查這個帳戶的名稱。他只會增加復雜性,使之更難理解。如果你想讓你的測試代碼更容易生存,更容易重構的話,那請確保你斷言的范圍。
寬松的順序
在比較集合和數組的時候你可能并不關心他們中元素的順序,通過使用ReflectionAssert.assertRefEquals方法并配合ReflectionComparatorMode.LENIENT_ORDER參數將忽略元素的順序。
List<Integer> myList = Arrays.asList(3, 2, 1); assertRefEquals(Arrays.asList(1, 2, 3), myList, LENIENT_ORDER);
無視默認
第二種的從寬方式是使用ReflectionComparatorMode.IGNORE_DEFAULTS模式,當這種模式被設置的時候,java
的默認值,如null、0、false將會不參與斷言(忽略)。
舉個例子,如果你有一個User類,該類有著first name,last name,street等屬性,但是你僅僅想對first name
和street進行檢查而忽略其他的屬性。
User actualUser = new User("John", "Doe", new Address("First street", "12", "Brussels")); User expectedUser = new User("John", null, new Address("First street", null, null)); assertRefEquals(expectedUser, actualUser, IGNORE_DEFAULTS);
你所期望忽略的屬性的對象必須放在斷言左邊,如果放在右邊那么依然進行比較。
assertRefEquals(null, anyObject, IGNORE_DEFAULTS); // Succeeds assertRefEquals(anyObject, null, IGNORE_DEFAULTS); // Fails
寬松的日期
第三種從寬處理是ReflectionComparatorMode.LENIENT_DATES,當兩個日期都是值,或者都是null的時候,實際的日期將會被忽略(即斷言為相等)。
Date actualDate = new Date(44444); Date expectedDate = new Date(); assertRefEquals(expectedDate, actualDate, LENIENT_DATES);
assertLenEquals
ReflectionAssert還提供了一種斷言,他提供寬松的順序又提供無視的忽略。
List<Integer> myList = Arrays.asList(3, 2, 1); assertLenEquals(Arrays.asList(1, 2, 3), myList); assertLenEquals(null, "any"); // Succeeds assertLenEquals("any", null); // Fails
屬性斷言
assertLenEquals和assertRefEquals都是比較對象,ReflectionAssert也提供方法對對象的屬性進行比較。(依賴與ONGL)。
一些屬性比較的例子
assertPropertyLenEquals("id", 1, user); //斷言user的id屬性的值是1 assertPropertyLenEquals("address.street", "First street", user); //斷言user的address的street屬性
在這個方式中你期望的值和判定的對象也可以使用集合
assertPropertyLenEquals("id", Arrays.asList(1, 2, 3), users); assertPropertyLenEquals("address.street", Arrays.asList("First street",
"Second street", "Third street"), users);
- 寫代碼,就一定要寫測試
- 不要受單元測試的教條所限
- 相信單元測試將會帶來的成果
- 統一考慮編碼和測試
- 測試比單元代碼重要
- 測試的最佳時機是代碼剛寫完之時
- 測試不會白費
- 當天有瑕疵的測試也比后補的完美測試好
- 不好的測試也比沒有測試強
- 測試有時可以驗證意圖
- 只有傻瓜不用工具
- 用好的去測試不好的
引至:Info中文網站http://www.infoq.com/cn/news/2007/04/savoia-tao-of-testing