有須要的能夠看一下(格式有點亂) html
一 Unitils簡介 java
單元測試應該很容易,直觀....至少在理論上是這樣的。 然而現實的項目一般跨越多個層次,有的是數據驅動有的使用中間件技術,好比EJB和Hibernate等等。 mysql
Unitils源於嘗試更加務實的單元測試,它始於一套測試準則,併爲了方便應用這些準則而開發了一個開源代碼庫。 spring
本文將經過一些實例向您展現如何在您的項目中使用Unitils。 sql
二 配置文件簡介 數據庫
unitils-default.properties 默認的配置,在unitils發行包中。 api
unitils.properties 可包含項目的所有配置 session
unitils-local.properties 能夠包含用戶特定配置 oracle
第一個配置文件unitils-default.properties,它包含了缺省值並被包含在unitils的發行包中。咱們沒有必要對這個文件進行修改,但它能夠用來做參考。 app
第二個配置文件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屬性)來開始咱們這一部分的介紹。
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是否相等了。
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方法,上面的代碼能夠重寫以下
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參數就能夠了。
好比:
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的值,其餘的屬性值你並不關心,那麼就能夠像下面這麼比較了。
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的值是否相等,若是你想嚴格比較對象的每個域,而又不想去比較時間的值是否是相等,那麼這種模式就是合適你的。
public void assertReflectionEqualsTest_LENIENT_DATES(){
Date actualDate = new Date(44444);
Date expectedDate = new Date();
ReflectionAssert.assertReflectionEquals(expectedDate,
actualDate, ReflectionComparatorMode.LENIENT_DATES);
}
ReflectionAssert類爲咱們提供了具備兩種寬鬆模式的斷言:既忽略順序又忽略缺省值的斷言assertLenientEquals,使用這種斷言上面兩個例子就能夠簡化以下了:
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.測試類編寫及具體實現
數據庫測試運行在一個單體測試數據庫上,它提供完整且易於管理的測試數據,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")
@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:用數據集裏的數據更新數據庫裏的數據,若是數據集裏
的數據不在數據庫中那麼失敗(好比一條數據擁有相同的主鍵值)。
在測試運行完以後用數據集中的數據去檢查數據庫中的數據,有時候這是很是有用的。好比你想檢查大塊數據更新和一個存儲過程的執行結果是否正確。
下面的例子是測試一個把插入用戶的的方法。
@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的狀況這裏再也不列出,能夠參照附件。
在上面的例子裏面咱們留下了一個重要的問題沒有說起:咱們測試數據庫用到數據源來自哪裏,而且咱們怎麼讓咱們測試的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()方法也能夠達到相同的目的。
因爲不一樣的緣由,數據庫的事物處理對於測試代碼是十分重要的,下面就是一些重要的緣由:
· 數據庫操做只有在事物處理的狀況下才運行,好比: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 數據源
能夠簡單的在一個類,方法或者屬性上加上@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裏會重用這個應用程序上下文。由於加載應用程序上下文是一個很是繁重的操做,若是重用這個應用程序上下文會大大提高測試代碼的性能。
只要配置好了應用程序上下文,全部以@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 objects可讓你測試一塊代碼而不用在乎這塊代碼所依賴的objects 和 services。到了unitils2.0版本,它提供了一套完整的動態生成mock objects的解決方案,並支持mock的建立和注入。
在unitils2.0版本以前,是使用EasyMock框架的,你也許會問爲何已經有像EasyMock這樣強大的Mock 對象應用庫,unitils還要寫一個完整的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被包裝到一個control 對象裏,這個control對象能夠定義行爲並調用斷言語句,在你的測試代碼裏聲明一個mock做爲屬性,而沒必要特地去實例化它。
Mock<MyService> mockService;
Unitils會建立mock control對象並在測試以前分配到域屬性裏,爲了得到mock control對象自己,只要調用control對象的getMock()方法。以下:
MyService myService = mockService.getMock();
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調用寬鬆的比較方法。
在測試中咱們常用域對象或者值對象,其實它們對於咱們的測試結果並不產生實際的影響。好比上面的AlertServiceTest,咱們須要兩個alert實例,在測試方法裏alerts從SchedulerService獲得而後傳遞給MessageService,由於沒有方法調用alert實例,因此alert實例並不重要。可是通常狀況下構造函數會強行要求咱們傳遞參數,並且要求咱們參數不爲Null。然而咱們使用的是mock objects,僅僅是個實例的代理,因此其實就須要個dummy instane(模擬對象)就能夠,若是想建立一個dummy instance能夠經過調用MockUnitils.createDummy方法或者在一個屬性前面加上 @Dummy 標籤。
Message alert1, alert2;
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
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;
固然也能夠在配置文件設置,這裏就再也不介紹。