單元測試:Unitils的簡單使用
一 Unitils簡介
單元測試應該很容易,直觀....至少在理論上是這樣的。 然而現實的項目通常跨越多個層次,有的是數據驅動有的使用中間件技術,比如EJB和Hibernate等等。
Unitils源于嘗試更加務實的單元測試,它始于一套測試準則,并為了方便應用這些準則而開發了一個開源代碼庫。
本文將通過一些實例向您展示如何在您的項目中使用Unitils。
二 配置文件簡介
unitils-default.properties 默認的配置,在unitils發行包中。
unitils.properties 可包含項目的全部配置
unitils-local.properties 可以包含用戶特定配置
第一個配置文件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
(以上3個文件在 配置文件 夾中,使用時將3個文件放在src下即可
三 Unitils 斷言應用
1.首先我們編寫我們測試中所使用的實體類(User Address)
User.java(User)
package unitils.assertflect;
public class User {
private int id;
private String firstName;
private String lastName;
private Address address
public User(int id,String firstName,String lastName){
this.id=id;
this.firstName=firstName;
this.lastName=lastName;
}
public User(String firstName, String lastName, Address address){
this.firstName=firstName;
this.lastName=lastName;
this.address=address;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
Address.java(Address)
package unitils.assertflect;
public class Address {
private String city;
private String num;
private String country;
public Address(String city,String num,String country){
this.city=city;
this.num=num;
this.country=country;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getNum() {
return num;
}
public void setNum(String num) {
this.num = num;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
}
2.編寫我們的測試類,并編寫測試方法
首先我們要將unitils-core文件夾下的jar 包導入到我們的工程中
測試類AssertTest的主體代碼
package unitils.assertflect;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import org.junit.Test;
import org.unitils.reflectionassert.ReflectionAssert;
import org.unitils.reflectionassert.ReflectionComparatorMode;
import static org.junit.Assert.assertEquals;
import junit.framework.TestCase;
public class AssertTest{
}
下面我們介紹一下各種斷言方式
應用反射的斷言
典型的單體測試一般都包含一個重要的組成部分:對比實際產生的結果和希望的結果是否一致的方法:斷言方法(assertEquals)。Unitils為我們提供了一個非常實用的assertion方法,讓我們用比較兩個USER對象的實例(User包括id ,firstName ,lastName,address屬性)來開始我們這一部分的介紹。
@Test
public void assertEquels(){
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertEquals(user1, user2);
因為兩個user包含相同的屬性,所以你一定以為斷言是成功的。但是事實恰恰相反,斷言失敗,因為user類沒有覆寫equals()方法,所以斷言就用判斷兩個對象是否相等來來返回結果,換句話說就是采用了user1 == user2的結果,用兩個對象的引用是否一致作為判斷的依據。
假如你像下面這樣重寫equals方法,
public boolean equals(Object object) {
if (object instanceof User) {
return id == ((User) object).id;
}
return false;
}
也許通過判斷兩個USER的ID是否相等來判斷這兩個user是否相等在您的程序邏輯里是行得通的,但是在單體測試里未必是有意義的,因為判斷兩個user是否相等被簡化成了user的id是否相等了。
@Test
public void assertEquels(){
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertEquals(user1, user2);
}
按照上面的代碼邏輯,也許斷言成功了,但是這是您期望的么?所以最好避免使用equals()方法來實現兩個對象的比較(除非對象的屬性都是基本類型)。對了,還有一個辦法也許能夠有效,那就是把對象的屬性一個一個的比較。
public void assertEquels() {
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertEquals(user1.getId(), user2.getId());
assertEquals(user1.getFirstName(), user2.getFirstName());
assertEquals(user1.getLastName(), user2.getLastName());
}
Unitils其實為我們提供了非常簡單的方法,一種采用反射的方法。使用ReflectionAssert.assertReflectionEquals方法,上面的代碼可以重寫如下
@Test
public void assertReflectionEqualsTest(){
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
ReflectionAssert.assertReflectionEquals(user1, user2);
}
這種斷言采用反射機制,循環的比較兩個對象的filed的值,比如上面的例子,它就是依次對比id,firstName,lastName的值是否相等。
如果某個filed本身就是object,那么斷言會遞歸的依次比對這兩個object的所有filed,對于Arrays ,Maps ,collection也是一樣的,會通過反射機制遞歸的比較所有的element,如果值的類型是基本類型(int, long, ...)或者基本類型的包裝類(Integer, Long, ...),就會比較值是否相等(using ==)。
看看下面的代碼,這回斷言成功了!
assertReflectionEquals(1, 1L);
List<Double> myList = new ArrayList<Double>();
myList.add(1.0);
myList.add(2.0);
assertReflectionEquals(Arrays.asList(1, 2), myList);
寬松式斷言
源于對代碼可維護性的原因,只添加對測試有益的斷言是十分重要的。讓我用一個例子來說明這一點:假如一個計算account balance的測試代碼,那么就沒有對bank-customer的name進行斷言的必要,因為這樣就增加了測試代碼的復雜度,讓人難于理解,更重要的是當代碼發生變化時增加了測試代碼的脆弱性。為了讓你的測試代碼更容易的適應其他代碼的重構,那么一定保證你的斷言和測試數據是建立在測試范圍之內的。
為了幫助我們寫出這樣的測試代碼,ReflectionAssert方法為我們提供了各種級別的寬松斷言。下面我們依次介紹這些級別的寬松斷言。
順序是寬松的
第一種寬松級別就是忽略collection 或者array中元素的順序。其實我們在應用list的時候往往對元素的順序是不關心的。比如:一個代碼想要搜索出所有無效的銀行賬號,那么返回的結果的順序就對我們業務邏輯沒什么影響。
為了實現這種寬松模式,ReflectionAssert.assertReflectionEquals方法可以通過配置來實現對順序的忽略,只要 ReflectionAssert.assertReflectionEquals方法設置 ReflectionComparatorMode.LENIENT_ORDER參數就可以了。
比如:
@Test
public void assertReflectionEqualsTest_LENIENT_ORDER(){
List<Integer> myList = Arrays.asList(3, 2, 1);
ReflectionAssert.assertReflectionEquals(
Arrays.asList(1, 2, 3),
myList, ReflectionComparatorMode.LENIENT_ORDER);
}
忽略缺省值
第二種寬松方式是:如果斷言方法被設置為ReflectionComparatorMode.IGNORE_DEFAULTS模式的話,java 的default values比如 objects 是null 值是 0 或者 false, 那么斷言忽略這些值的比較,換句話說就是斷言只會比較那些你初始化了的期望值,如果你沒有初始化一些filed,那么斷言就不會去比較它們。
還是拿個例子說明比較好,假設有一個user類:有firstName, lastName,city... field屬性,但是你只想比較兩個對象實例的first name和street的值,其他的屬性值你并不關心,那么就可以像下面這么比較了。
@Test
public void assertReflectionEqualsTest_IGNORE_DEFAULTS(){
User actualUser = new User("John", "Doe", new
Address("First city", "12", "Brussels"));
User expectedUser = new User("John", null, new
Address("First city", null, null));
ReflectionAssert.assertReflectionEquals(expectedUser,
actualUser, ReflectionComparatorMode.IGNORE_DEFAULTS);
}
你想忽略的屬性值設置為null那么一定把它放到左邊參數位置(=expected),如果只有右邊參數的值為null,那么斷言仍然會比較的。
assertReflectionEquals(null, anyObject, IGNORE_DEFAULTS);
// Succeeds
assertReflectionEquals(anyObject, null, IGNORE_DEFAULTS);
// Fails
寬松的date
第三種寬松模式是ReflectionComparatorMode.LENIENT_DATES,這種模式只會比較兩個實例的date是不是都被設置了值或者都為null, 而忽略date的值是否相等,如果你想嚴格比較對象的每一個域,而又不想去比較時間的值是不是相等,那么這種模式就是合適你的。
@Test
public void assertReflectionEqualsTest_LENIENT_DATES(){
Date actualDate = new Date(44444);
Date expectedDate = new Date();
ReflectionAssert.assertReflectionEquals(expectedDate,
actualDate, ReflectionComparatorMode.LENIENT_DATES);
}
assertLenientEquals方法
ReflectionAssert類為我們提供了具有兩種寬松模式的斷言:既忽略順序又忽略缺省值的斷言assertLenientEquals,使用這種斷言上面兩個例子就可以簡化如下了:
@Test
public void assertLenientEqualsTest(){
List<Integer> myList = Arrays.asList(3, 2, 1);
ReflectionAssert.assertLenientEquals(Arrays.asList(1, 2, 3),
myList);
//ReflectionAssert.assertLenientEquals(null,"any");// Succeeds
ReflectionAssert.assertLenientEquals("any", null); // Fails
}
assertReflection ...以這種方式命名的斷言是默認嚴格模式但是可以手動設置寬松模式的斷言,assertLenient ...以這種方式命名的斷言是具有忽略順序和忽略缺省值的斷言。
屬性斷言
assertLenientEquals和 assertReflectionEquals這兩個方法是把對象作為整體進行比較,ReflectionAssert類還給我們提供了只比較對象的特定屬性的方法:assertPropertyLenientEquals 和 assertPropertyReflectionEquals,比如:
assertPropertyLenientEquals("id", 1, user);
assertPropertyLenientEquals("address.city", "First city", user);
這個方法的參數也支持集合對象,下面的例子就會比較特定的屬性的集合中的每一個元素是否相等。
assertPropertyLenientEquals("id", Arrays.asList(1, 2, 3), users);
assertPropertyLenientEquals("address.city", Arrays.asList("First city",
"Second city", "Third city"), users);
同樣每一種方法都提供兩個版本,assertPropertyReflection Equals 和assertPropertyLenient Equals . assertPropertyReflection...以這種方式命名的斷言是默認嚴格模式但是可以手動設置寬松模式的斷言,assertPropertyLenient...以這種方式命名的斷言是具有忽略順序和忽略缺省值的斷言。
四 Unitils 數據庫應用
對于商業應用程序來說數據庫層的單體測試是十分重要的,但是卻常常被放棄了,因為太復雜了。Unitils大大減少了這種復雜度而且可維護。下面就介紹支持DatabaseModule 和DbUnitModule的數據庫測試。
為了方便,本實例是使用Hibernate實現的數據庫的讀寫操作。數據庫為mysql,使用數據庫名test,表名User(當然這里可以不用hibernate來實現)
個人理解:Unitils 數據庫應目的是為了測試我們的dao操作方法的正確性,以簡單的查詢操作方法為例。在進行測試時我們需要建立一個xml文件作為測試數據集,Unitil會將數據集中的數據插入到數據庫中,此時我們用被測試的查詢操作來讀取數據庫中的數據,讀出數據后,我們可以用斷言的方式將我們數據集中的數據與讀出的數據進行比較。如果,斷言成功,說明我們的查詢方法正確。
1.創建表、編寫我們的dao類及配置相關文件
(1)首先我們創建數據庫,并創建所需要的表
CREATE DATABASE TEST;
USER TEST;
CREATE TABLE USER(
ID INT(11) PRIMARY KEY,
NAME VARCHAR(20),
GENDER VARCHAR(20)
)
(2)編寫我們的dao操作
導入hibernate所使用的jar包(注意這里使用的不是Unitils提供的)
導入unitils-database和unitils-dbunit文件夾下的jar包
User.java
package jc.com.unitils.dao.by.dbunit;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
public class User {
@Id
private int id;
private String name;
private String gender;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
}
UserDAO.java
package jc.com.unitils.dao.by.dbunit;
import java.util.List;
public interface UserDAO {
public void insertUser(User user);
public User getUser(User user);
public void deleteUer(User user);
}
UserDAOImpl.java
package jc.com.unitils.dao.by.dbunit;
import java.util.List;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.cfg.Configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.unitils.database.annotations.Transactional;
@Repository("userDAO")
public class UserDAOImpl implements UserDAO {
private static UserDAOImpl udi ;
@Autowired
private SessionFactory sessionFactory;
private SessionFactory sf;
private static Session session;
public static UserDAOImpl getInstanceUserDAOImpl(){
if(udi != null){
return udi;
}else{
udi=new UserDAOImpl();
return udi;
}
}
public void Init(){
Configuration cfg=new AnnotationConfiguration();
sf=cfg.configure().buildSessionFactory();
session=sf.openSession();
session.beginTransaction();
}
public void Destroy(){
session.getTransaction().commit();
session.close();
sf.close();
}
@Override
public void insertUser(User user) {
// TODO Auto-generated method stub
session.save(user);
}
@Override
public void deleteUer(User user){
session.delete(user);
}
@Override
public User getUser(User user) {
// TODO Auto-generated method stub
return (User)session.get(User.class, user.getId());
}
}
Hibernate.cfg.xml文件
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property
name="hibernate.connection.url">jdbc:mysql://localhost/test</property>
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password">sa</property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
<property name="hibernate.show_sql">true</property>
<property name="hbm2ddl.auto">update</property>
<mapping class = "jc.com.unitils.dao.by.dbunit.User"/>
</session-factory>
</hibernate-configuration>
好了,完成以上操作后我們hibernate框架就搭建好了,即編寫完了我們的被測試dao操作
(3)配置文件相關文件
首先將配置文件夾下的所有文件拷貝到classpath下,我們拷貝到src下即可
其次我們修改一下配置文件(這里我們僅是簡單配置,主要是為了滿足我們實例的需求)
Unitils.properties文件
將localfile文件修改為unitils-default文件,當然我們這里沒有用到unitils-default.properties文件(主要原因是所使用的unitils.properties文件缺少了對unitils-default.properties文件的配置,所以我們這里借用unitils-default文件的配置)
unitils.configuration.localFileName=unitils-default.properties
添加數據庫配置
# Properties for the PropertiesDataSourceFactory
database.driverClassName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/test
database.userName=root
database.password=sa
database.schemaNames=test
database.dialect=mysql
同樣修改unitils-default.properties中的數據庫配置
特別注意:
unitils.module.database.className=org.unitils.database.DatabaseModule
unitils.module.database.runAfter=false//需設置false,否則我們的測試函數只有在執行完函數體后,才將數據插入的數據表表中
unitils.module.database.enabled=true
2.測試類編寫及具體實現
通過DbUnit來管理測試數據
數據庫測試運行在一個單體測試數據庫上,它提供完整且易于管理的測試數據,DbUnitModule在Dbunit的基礎上提供對測試數據集的支持。
加載測試數據集
還是讓我們以一個簡單的例子開始,getUser方法通過一個僅有User(僅已設置Id)獲取完整的User信息。典型的測試代碼如下:
package jc.com.unitils.dao.by.dbunit;
import junit.framework.TestCase;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.AnnotationConfiguration;
import org.junit.Test;
import org.unitils.UnitilsJUnit4;
import org.unitils.database.annotations.TestDataSource;
import org.unitils.database.annotations.Transactional;
import org.unitils.database.util.TransactionMode;
import org.unitils.dbunit.annotation.DataSet;
import org.unitils.dbunit.annotation.ExpectedDataSet;
import org.unitils.reflectionassert.ReflectionAssert;
@DataSet
public class UserDAOTest extends UnitilsJUnit4{
// @ExpectedDataSet("user1.xml")
@Test
@DataSet("user.xml")
public void testGetUserById(){
UserDAOImpl udi=UserDAOImpl.getInstanceUserDAOImpl();
udi.Init();
User example=new User();
example.setId(3);
example=udi.getUser(example);
udi.Destroy();
ReflectionAssert.assertPropertyLenientEquals("name", "jc",
example);
}
添加以上代碼后,我們運行是會報錯的,因為我們還沒有添加需要的數據集文件user.xml,下面就先讓我們了解一下關于數據集的內容
在測試代碼中加入@DataSet標簽,Unitils就會為測試代碼找測試需要的DbUnit數據文件。如果沒有指定文件名,Unitils會自動在測試類目錄下查找以className .xml格式命名的測試數據集文件。
數據集文件將采用DbUnit's FlatXMLDataSet文件格式并包含測試需要的所有數據。表中的所有數據集的內容首先將被刪除,然后所有數據集中的數據將被插入到表中,數據集里沒有的表將不會被清空,如果你想清空指定的表,可以在數據集文件里添加一個空的表元素,比如在數據集文件里添加<MY_TABLE />,如果你想指定為一個null值,你可以用【null】設置。
一種方法是建立一個類級的數據集文件,以UserDAOTest.xml命名,然后放到UserDAOTest相同目錄下(第一種方法我們這里沒有使用)
另一種是假設getUser()方法需要一個特殊的測試數據集而不是類級的測試數據集,那么我們建立一個名字為UserDAOTest.getUser.xml的數據集文件并放到測試類的目錄下。
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<user id="3" name="jc" gender="student"/>
</dataset>
我們在以上方法前加入了@DataSet標簽,就會重寫默認的數據集文件,會使用我們的user.xml。
所以我們需要在UserDAOTest同目錄下建立user.xml文件,內容如下
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<user id="3" name="jc" gender="student"/>
</dataset>
這下好了,我們可以執行測試了。
方法級的數據集不能被重用,因為越多的數據集文件就意味著更多的維護。開始你也許會重用類級的數據集,絕大多數的情況下一個小的數據集會在很多測試中被重用,但是如果這樣的話,就會出現一個非常大的而且數據相關性很小的數據集,也許每個方法單獨使用一個數據集會好些,或者把一個測試拆分為幾個測試。
配置數據集加載策略
默認的數據集加載到數據庫里采用clean insert策略,也就是先把數據庫中的數據clean,然
后把數據集的數據insert到數據庫。詳細的過程是:數據集里有的表都會在數據庫中清空,然后把數據集里的測試數據插入到表中。這種行為是可以被配置的,通過修改屬性DbUnitModule.DataSet.loadStrategy.default可以實現。比如我們修改如下屬性:
DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.InsertLoadStrategy
這種策略是用insert 代替 clean insert,也就是數據集里的表在數據庫里不被刪除而只是把數據集里的測試數據插入到數據庫中。
這種加載策略也可以在特點的測試用例上使用,通過修改@DataSet標簽的屬性值。
@DataSet(loadStrategy = InsertLoadStrategy.class)
因為這個和DbUnit是類似的,采用不同的加載策略就是使用不同的數據庫操作。下面的加載策略是默認支持的。
(1)CleanInsertLoadStrategy:數據集里有的表在數據庫中都要把數據刪掉,然
后把數據集里的數據插入到數據庫中。
(2)InsertLoadStrategy:就是簡單的把數據集里的數據插入到數據庫中。
(3)RefreshLoadStrategy:用數據集里的數據更新數據庫中的數據。也就是:數
據集里有數據庫也有的數據被更新,數據集里有而數據庫里沒有的數據被插入,
數據庫里面有而數據集里沒有的數據保持不變。
(4)UpdateLoadStrategy:用數據集里的數據更新數據庫里的數據,如果數據集里
的數據不在數據庫中那么失敗(比如一條數據擁有相同的主鍵值)。
證實測試結果
在測試運行完之后用數據集中的數據去檢查數據庫中的數據,有時候這是非常有用的。比如你想檢查大塊數據更新和一個存儲過程的執行結果是否正確。
下面的例子是測試一個把插入用戶的的方法。
@Test
@ExpectedDataSet("user1.xml")
public void testInsertUser(){
UserDAOImpl udi=UserDAOImpl.getInstanceUserDAOImpl();
udi.Init();
User example=new User();
example.setId(4);
example.setName("jc4");
example.setGender("student");
udi.insertUser(example);
udi.Destroy();
}
注意我們在測試方法上面加了一個@ExpectedDataSet標簽,它會告訴Unitils去找一個叫做user.xml數據集文件,并且去比較數據集里的數據和數據庫中的數據。
User.xml中的數據如下
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<user id="3" name="jc" gender="student"/>
<user id="4" name="jc4" gender="student"/>
</dataset>
對于這個數據集它會去檢查數據庫的表中是否有和這兩條數據相同的數據記錄。
和@DataSet標簽一樣,文件名可以被指定,如果沒有指定文件名就會采用下面的命名規則:className .methodName -result.xml。
使用的數據集盡量最小化,增加數據量也就意味著更多的維護。作為一種變通,你可以在不同的測試中采用相同的檢查數據。
對于multi-schema的情況這里不再列出,可以參照附件。
連接到測試數據庫(由于這里用的Hibernate沒有使用該方法)
在上面的例子里面我們留下了一個重要的問題沒有提及:我們測試數據庫用到數據源來自哪里,并且我們怎么讓我們測試的DAO類來使用我們的數據源。
當我們開始我們的測試實例的時候,Unitils會根據我們定義的屬性來創建一個數據源實例連接到我們的測試數據庫。隨后的數據庫測試會重用相同的數據源實例。建立連接的細節定義在下面的屬性里:
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
我們在工程的unitils.properties文件里設置driver和 url這兩個屬性,這是為整個工程使用的屬性,如果特定用戶使用的屬性我們可以設置在unitils-local.properties文件里,比如user, password 和 schema,這樣每個開發者就使用自己定義的測試數據庫的schema,而且彼此之間也不回產生影響。
在一個測試被建立之前,數據源要注入到測試實例中:如果在一個屬性或者setter方法前發現@TestDataSource標簽就會設置或者調用數據源實例。你必須為你的測試代碼加上配置代碼,好讓你的測試類使用數據源,一般的都通過繼承一個基類實現數據庫測試,下面就是一個典型基類的代碼:
public abstract class BaseDAOTest extends UnitilsJUnit4 {
@TestDataSource
private DataSource dataSource;
@Before
public void initializeDao() {
BaseDAO dao = getDaoUnderTest();
dao.setDataSource(dataSource);
}
protected abstract BaseDAO getDaoUnderTest();
}
上面的例子是用一個標簽來獲得數據源的引用,調用DatabaseUnitils.getDataSource()方法也可以達到相同的目的。
事物(Transactions)
由于不同的原因,數據庫的事物處理對于測試代碼是十分重要的,下面就是一些重要的原因:
· 數據庫操作只有在事物處理的情況下才運行,比如:SELECT FOR UPDATE or triggers that execute ON COMMIT。
· 很多工程的測試代碼在運行之前需要填充一些數據來達到測試的目的,在測試過程中數據庫中的數據會被插入或者修改,為了在每一次測試前數據庫都在一個特定的狀態下,我們測試之前開始一個事物,測試之后回滾到起始狀態。
· 如果你的項目應用Hibernate或者JPA,那么這些框架都要在一個事物下測試才能夠保證系統運行正常。
默認情況下每一次測試都執行一個事物,在測試結束的時候commit。
這種默認情況可以通過修改屬性來改變,比如:
DatabaseModule.Transactional.value.default=disabled
這個屬性的其他合法設置值可以是:commit, rollback 和 disabled。
事物的行為也可以通過加入@Transactional標簽在測試類級別修改。
比如:
@Transactional(TransactionMode.ROLLBACK)
public class UserDaoTest extends UnitilsJUnit4 {
這樣的話,在每一次測試結束后都會回滾,@Transactional這個標簽是可繼承的,所以可以在公共父類里定義,而不是在每個類里單獨定義。
其實Unitils是依靠Spring來進行事物管理的,但是這并不意味著你必須在你的代碼里加入Spring來進行事物管理,事實上是使用了Spring進行事物管理但是這一切都是透明的。
如果使用unitils對Spring的支持,可以在Spring的配置文件里設置一個PlatformTransactionManager類型的Bean ,unitils就會用它做事物管理器。
五 應用 Spring 測試
Unitils提供了一些在Spring框架下進行單體測試的特性。Spring的一個基本特性就是:類要設計成為:沒有Spring容器或者在其他容器下仍然易于進行單體測試。但是很多時候在Spring容器下進行測試還是非常有用的。
Unitils提供了以下支持Spring的特性:
· ApplicationContext配置的管理
· 在單體測試代碼中注入Spring的Beans
· 使用定義在Spring配置文件里的Hibernate SessionFactory
· 引用在Spring配置中Unitils 數據源
ApplicationContext配置
可以簡單的在一個類,方法或者屬性上加上@SpringApplicationContext標簽,并用Spring的配置文件作為參數,來加載應用程序上下文。下面就是一個例子:
public class UserServiceTest extends UnitilsJUnit4 {
@SpringApplicationContext({"spring-config.xml", "spring-test-config.xml"})
private ApplicationContext applicationContext;
}
加載spring-config.xml 和 spring-test-config.xml這兩個配置文件來生成一個應用程序上下文并注入到加注解的域范圍里,在setter方法加注解一樣可以達到注入應用程序上下文的目的。
加載應用程序上下文的過程是:首先掃描父類的@SpringApplicationContext標簽,如果找到了就在加載子類的配置文件之前加載父類的配置文件,這樣就可以讓子類重寫配置文件和加載特定配置文件。比如:
@SpringApplicationContext("spring-beans.xml")
public class BaseServiceTest extends UnitilsJUnit4 {
}
public class UserServiceTest extends BaseServiceTest {
@SpringApplicationContext("extra-spring-beans.xml")
private ApplicationContext applicationContext;
}
上面的例子創建了一個新的應用程序上下文,它首先加載spring-beans.xml配置文件,然后加載extra-spring-beans.xml配置文件,這個應用程序上下文會注入到加入標簽的屬性里。
注意上面的例子,創建了一個新的應用程序上下文,這么做是因為要為這個類加載指定的配置文件。Unitils會盡可能的重用應用程序上下文,比如下面的例子沒有加載新的配置文件,所以就重用相同的實例。
@SpringApplicationContext("spring-beans.xml")
public class BaseServiceTest extends UnitilsJUnit4 {
}
public class UserServiceTest extends BaseServiceTest {
@SpringApplicationContext
private ApplicationContext applicationContext;
}
public class UserGroupServiceTest extends BaseServiceTest {
@SpringApplicationContext
private ApplicationContext applicationContext;
}
在父類BaseServiceTest里指定了配置文件,應用程序上下文會創建一次,然后在子類 UserServiceTest 和 UserGroupServiceTest里會重用這個應用程序上下文。因為加載應用程序上下文是一個非常繁重的操作,如果重用這個應用程序上下文會大大提升測試代碼的性能。
注入Spring的Beans
只要配置好了應用程序上下文,所有以@SpringBean , @SpringBeanByType 或者@SpringBeanByName注釋的fields / setters都會注入beans,下面的例子展示了如何根據應用程序上下文來獲得UserService bean實例。
@SpringBean("userService")
private UserService userService;
@SpringBeanByName
private UserService userService;
@SpringBeanByType
private UserService userService;
用@SpringBean標簽你可以從應用程序上下文得到一個具有獨一無二名字的Spring的bean, @SpringBeanByName這個標簽效果相同,只是它根據類field名稱來區分bean。
當使用@SpringBeanByType標簽的時候,應用程序上下文會查找一個和filed類型相同的bean,這個例子中,會查找UserService類或者子類的bean,如果這樣的bean不存在或者不只找到一個結果,那么拋出異常。
在setter上面也可以使用相同的標簽,比如:
@SpringBeanByType
public void setUserService(UserService userService) {
this.userService = userService;
}
應用Mock(模擬)對象進行測試
單體測試是要把測試代碼隔離開的,Mock objects可以讓你測試一塊代碼而不用在意這塊代碼所依賴的objects 和 services。到了unitils2.0版本,它提供了一套完整的動態生成mock objects的解決方案,并支持mock的創建和注入。
在unitils2.0版本之前,是使用EasyMock框架的,你也許會問為什么已經有像EasyMock這樣強大的Mock 對象應用庫,unitils還要寫一個完整的Mock模塊呢?一個重要的原因就是它想提供一個大大改進的并且用戶友好性更強的庫。
Mock測試實例
下面是測試alert service的實例:sendScheduledAlerts()方法需要從AlertSchedulerService獲取所有的scheduled alerts,然后把它們傳遞給MessageSenderService。
public class AlertServiceTest extends UnitilsJUnit4 {
AlertService alertService;
Message alert1, alert2;
List<Message> alerts;
Mock<SchedulerService> mockSchedulerService;
Mock<MessageService> mockMessageService;
@Before
public void init() {
alertService = new AlertService(mockSchedulerService.getMock(), mockMessageService.getMock());
alert1 = new Alert(...); alert2 = new Alert(...);
alerts = Arrays.asList(alert1, alert2);
}
這個測試實例使用SchedulerService 和 MessageService的mock(模擬)。在測試代碼的第一個語句中:
mockSchedulerService.returns(alerts).getScheduledAlerts(null));
首先指定接下來調用SchedulerService mock對象的getScheduledAlerts方法時將返回包括alert1和alert2的List對象alerts,而且getScheduledAlerts方法的參數是任意的(因為設置行為的參數是Null就意味著任意值)。接下來的測試代碼調用這個方法:
alertService.sendScheduledAlerts();
然后調用斷言語言,檢查在mockMessageService對象里的sendAlert方法是不是以alert1和alert2為參數被調用了。
mock objects實例化
mock objects被包裝到一個control 對象里,這個control對象可以定義行為并調用斷言語句,在你的測試代碼里聲明一個mock作為屬性,而不必特意去實例化它。
Mock<MyService> mockService;
Unitils會創建mock control對象并在測試之前分配到域屬性里,為了獲得mock control對象本身,只要調用control對象的getMock()方法。如下:
MyService myService = mockService.getMock();
定義Mock行為動作
Unitils提供了簡單明了的定義mock行為動作的語法,以myUser 為參數調用getScheduledAlerts 方法返回alerts ,我們可以簡單的定義如下:
mockSchedulerService.returns(alerts).getScheduledAlerts(myUser);
還可以定義拋出的異常:
mockSchedulerService.raises(new
BackEndNotAvailableException()).getScheduledAlerts(myUser);
你也可以像下面這樣指定異常類:
mockSchedulerService.raises(BackEndNotAvailableException.class).getScheduledAlerts(myUser);
你也可以向下面這樣指定用戶行為
mockSchedulerService.performs(new MockBehavior() {
public Object execute(ProxyInvocation mockInvocation) {
// ... (retrieve alerts logic)
return alerts;
}
});:
如果相同的方法要在不同的調用中執行不同的行為,那么你就必須在定義行為時通過調用onceReturns , onceRaises 或者 oncePerforms讓它只適用一次。比如:
mockSchedulerService.onceReturns(alerts).getScheduledAlerts(myUser);
mockSchedulerService.onceRaises(new BackEndNotAvailableException()).getScheduledAlerts(myUser);
如果你用 returns 和 raises 代替 onceReturns 和onceRaises,那么第二次定義的行為永遠也不會被調用(這種情況下,永遠調用第一次定義的行為)。
由于可維護性的原因,我們盡量不使用once這個語法,因為假想的方法調用順序使你的測試代碼變得脆弱。如果可能可以使用調用相同的函數使用不同參數的辦法來解決上面的問題。
驗證期望的調用
測試方法執行完之后,往往我們想查看mock objects的一些我們期望的方法是不是被調用了。比如:
mockMessageService.assertInvoked().sendMessage(alert1);
這個方法驗證了mock MessageService 中的sentMessage方法是否被調用,并且是以alert1為參數。注意這個斷言只能執行一次,如果反復調用這個斷言,Unitils就會認為這個方法是不是被調用了兩次。
Unitils默認情況下是不支持驗證不期望情況是否發生,可以明確的調用像這樣的方法:assertNotInvoked來驗證方法沒被調用。比如,我們可以像下面這樣驗證3號alert沒有被發送:
mockMessagService.assertNotInvoked().sendMessage(alert3);
為了驗證你的mock對象沒有接受其他的方法調用,你可以用下面的靜態方法:MockUnitils.assertNoMoreInvocations();
默認情況下方法的調用順序是不會被檢查的,但是如果你想測試方法的調用順序,你可以使用assertInvokedInSequence,比如你想證實alert1是在alert2之前被調用的,就可以像下面這樣寫:
mockMessageService.assertInvokedInSequence().sendMessage(alert1);
mockMessageService.assertInvokedInSequence().sendMessage(alert2);
參數匹配
為了使測試變得易維護并簡單,參數的值沒有要求是最好的。Unitils為我們提供了一個最簡單的方法來忽略參數的值,那就是:參數值設為Null。getScheduledAlerts方法的user參數如果想被 忽略的話可以這樣寫:
mockSchedulerService.returns(alerts).getScheduledAlerts(null));
注意,如果傳遞的參數是對象引用,那么期望值和實際值的比較采用寬松的反射比較方法:通過反射來比較引用,如果屬性是:null,0或者false那么忽略這些屬性,忽略集合元素的順序(參照寬松斷言的介紹)。
如果你還想采用其他的參數匹配方法,那么你可以使用參數匹配器。在org.unitils.mock.ArgumentMatchers下面提供了一系列的參數匹配器,靜態引用這個類可以得到一套的靜態方法。
mockSchedulerService.returns(alerts).getScheduledAlerts(notNull(User.class))); // Matches with any not-null object of type User
mockSchedulerService.returns(alerts).getScheduledAlerts(isNull(User.class))); // The argument must be null
mockMessageService.assertInvoked().sendMessage(same(alert1)); // The argument must refer to alert1 instance
其他的參數匹配器還有:eq , refEq 和 lenEq。如果使用eq, 調用equals()方法來判斷實際的參數和期望的參數是否一致,使用refEq調用嚴格的反射比較方法,使用lenEq調用寬松的比較方法。
Dummy objects(模擬對象)
在測試中我們經常使用域對象或者值對象,其實它們對于我們的測試結果并不產生實際的影響。比如上面的AlertServiceTest,我們需要兩個alert實例,在測試方法里alerts從SchedulerService得到然后傳遞給MessageService,因為沒有方法調用alert實例,所以alert實例并不重要。但是一般情況下構造函數會強行要求我們傳遞參數,而且要求我們參數不為Null。然而我們使用的是mock objects,僅僅是個實例的代理,所以其實就需要個dummy instane(模擬對象)就可以,如果想創建一個dummy instance可以通過調用MockUnitils.createDummy方法或者在一個屬性前面加上 @Dummy 標簽。
@Dummy
Message alert1, alert2;
Mock 注入
Unitils為我們提供了很多mock注入方法,下面的例子為我們展示了如何建立UserDao Mock并注入到UserService中。
@InjectInto(property="userDao")
private Mock<UserDao> mockUserDao;
@TestedObject
private UserService userService;
上面例子中的語句,在setup()之后測試的代碼之前,@Inject標簽會使mockUserDao注入到userService的userDao屬性中。支持Getter, setter 和 field access ,而且也支持private access。
通過@TestedObject標簽聲明了的域就是注入的目標,如果通過@TestedObject標簽聲明了多個域,那么每一個域都將被注入mock對象。如果測試的對象還不存在,那么自動創建一個實例,如果無法完成注入,比如測試類里不存在標簽指定的類型或者測試類不能創建,那么測試拋出UnitilsException異常并且測試失敗。
如果需要也可以通過設置注入標簽的屬性來指定注入的目標。比如:
@InjectInto(target="userService", property="userDao")
private Mock<UserDao> mockUserDao;
private UserService userService;
上一個例子說明了怎么通過目標屬性的名稱來準確的注入Mock對象,Unitils也支持通過類型自動把objects注入。
@InjectIntoByType
private Mock<UserDao> mockUserDao;
@TestedObject
private UserService userService;
mockUserDao將被注入到userService的一個屬性里,這個屬性的類型是UserDao,或者它的屬性是UserDao的超類和接口。如果注入的候選者不止一個,那么選擇最復合條件的域,如果沒有這么一個最佳候選者,那么拋出UnitilsException異常并且測試失敗。
靜態注入
有很多和@InjectInto ,@InjectIntoByType相對應的用來為靜態的域或者setter方法注入mock對象的標簽,比如:@InjectIntoStatic 和 @InjectIntoStaticByType。這些標簽一般用來把mock注入到單例的類里。比如:
@InjectIntoByType
private Mock<UserDao> mockUserDao;
@TestedObject
private UserService userService;
上面的例子會把創建的mock對象注入到UserService類的靜態單例域里。
如果只有這么一個操作執行了,那么測試代碼會使UserService處于一個非法的狀態下,因為其他的測試如果進入相同單例的域里mock會代替真正的 user service。為了解決這個問題,unitils在測試執行完后會還原這個域的原始值。在上面的例子里在測試執行完成后,單例的域 UserService會被還原到以前的實例或者Null。
實際還原的動作可以通過標簽來指定,可以選擇還原到原始值(default),或者把這個域設置為null或者0,還可以指定這個域保持不變。比如:
@InjectIntoStaticByType(target=UserService.class restore=Restore.NULL_OR_0_VALUE)
private Mock<UserDao> mockUserDao;
也可以通過設置配置屬性,改變默認值,比如可以設置還原的動作是在項目范圍內所有一次測試的mock對象。
InjectModule.InjectIntoStatic.Restore.default=old_value
支持EasyMock
Unitils也提供對EasyMock的支持,EasyMock提供了便捷有效的方法來減輕創建Mock,匹配參數和注入mock等操作的復雜性。
如果一個域被@Mock標簽注釋了那么就會生成一個Mock,Unitisl會創建一個和被注釋了的域類型相同的mock并注入到這個域里。這個mock 的創建和賦值應該在測試的setup之前完成,在setup過程中你應該完成額外的配置工作,比如裝入mock好讓你在測試中使用它。前面的部分已經介紹了Unitils是如何幫助你簡單的注入Mock對象。
下面的例子展示了一個關于UserService的單體測試代碼,UserService是把在一定時間內沒有進行活動的帳號注銷。
public class UserServiceTest extends UnitilsJUnit4 {
@Mock
private UserDao mockUserDao;
private UserService userService;
@Before
public void setUp() {
userService = new UserService();
userService.setUserDao(mockUserDao);
}
@Test
testDisableInActiveAccounts() {
expect(mockUserDao.getAccountsNotAccessedAfter(null)).andReturn(accounts);
mockUserDao.disableAccount(accounts.get(0));
mockUserDao.disableAccount(accounts.get(1));
EasyMockUnitils.replay();
userService.disableInactiveAccounts();
}
}
在這個例子中user service的UserDao被一個mock 對象代替,這個mock對象是Unitils自動創建并在測試代碼的setup過程中裝入到user service中的,測試過程中我們首先記錄下來我們希望的操作,然后調用lEasyMockUnitils.replay()方法,他會replay所以mock對象(這個例子中只有mockUserDao)。然后執行實際的測試代碼,測試之后Unitils會調用 EasyMockUnitils.verify()方法來驗證所有的mock對象是否執行了希望的操作。
默認創建的mock對象使用EasyMock的比較嚴格比對方法(比如,如果不期望的方法被調用了那么測試失敗),而且忽略了方法調用的順序,你也可以通過設置@Mock標簽的屬性值來指定設置。
@Mock(returns=Calls.LENIENT, invocationOrder=InvocationOrder.STRICT)
private UserDao mockUserDao;
也可以通過修改配置文件來改變所有@Mock標簽的默認值。
EasyMockModule.Mock.Calls.default=lenient
EasyMockModule.Mock.InvocationOrder.default=strict
通過反射機制的寬松參數匹配
Unitils提供的mock對象和直接使用EasyMock對象還是有一些差別的:比如EasyMock使用的是LenientMocksControl,這個控制器是通過反射機制進行方法調用的參數匹配的,而且是寬松的匹配,下面的方法都是可以匹配的
expected: dao.findById(0);
actual: dao.findById(99999);
List<Integer> userIds = new ArrayList<Integer>();
userIds.add(3);
userIds.add(2);
userIds.add(1);
expected: dao.deleteById(Arrays.asList(1,2,3));
actual: dao.deleteById(userIds);
expected: dao.update(0, new User(null, "Doe"));
actual: dao.update(9999, new User("John", "Doe"));:
正如你所看到的寬松匹配不僅僅在對象和對象域中體現而且在方法的參數匹配上也一樣是寬松的匹配。下面的例子:
expect(mockUserDao.getAccountsNotAccessedAfter(null)).andReturn(accounts);
方法的參數如果被設置為null,那么就意味著我們并不關心傳遞給方法的參數究竟是什么。你也可以通過設置標簽@Mock的屬性來改變匹配的寬松度,比如:
@Mock(order=Order.STRICT, defaults=Defaults.STRICT, dates=Dates.LENIENT)
private UserDao mockUserDao;
當然也可以在配置文件設置,這里就不再介紹。