Unitils的简单实用

有须要的能够看一下(格式有点乱)  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属性)来开始咱们这一部分的介绍。

@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;

}

也许经过判断两个USERID是否相等来判断这两个user是否相等在您的程序逻辑里是行得通的,可是在单体测试里未必是有意义的,由于判断两个user是否相等被简化成了userid是否相等了



@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 namestreet的值,其余的属性值你并不关心,那么就能够像下面这么比较了。




@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-databaseunitils-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配置的管理

· 在单体测试代码中注入SpringBeans

· 使用定义在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里会重用这个应用程序上下文。由于加载应用程序上下文是一个很是繁重的操做,若是重用这个应用程序上下文会大大提高测试代码的性能。

注入SpringBeans


只要配置好了应用程序上下文,全部以@SpringBean , @SpringBeanByType 或者@SpringBeanByName注释的fields / setters都会注入beans,下面的例子展现了如何根据应用程序上下文来得到UserService bean实例。


@SpringBean("userService")

private UserService userService;


@SpringBeanByName

private UserService userService;


@SpringBeanByType

private UserService userService;


用@SpringBean标签你能够从应用程序上下文获得一个具备独一无二名字的Springbean, @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方法时将返回包括alert1alert2List对象alerts,并且getScheduledAlerts方法的参数是任意的(由于设置行为的参数是Null就意味着任意值)。接下来的测试代码调用这个方法:


       alertService.sendScheduledAlerts();


而后调用断言语言,检查在mockMessageService对象里的sendAlert方法是否是以alert1alert2为参数被调用了。


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来验证方法没被调用。好比,咱们能够像下面这样验证3alert没有被发送:


mockMessagService.assertNotInvoked().sendMessage(alert3);


为了验证你的mock对象没有接受其余的方法调用,你能够用下面的静态方法:MockUnitils.assertNoMoreInvocations();


默认状况下方法的调用顺序是不会被检查的,可是若是你想测试方法的调用顺序,你可使用assertInvokedInSequence,好比你想证明alert1是在alert2以前被调用的,就能够像下面这样写:


mockMessageService.assertInvokedInSequence().sendMessage(alert1);

mockMessageService.assertInvokedInSequence().sendMessage(alert2);


参数匹配


为了使测试变得易维护并简单,参数的值没有要求是最好的。Unitils为咱们提供了一个最简单的方法来忽略参数的值,那就是:参数值设为NullgetScheduledAlerts方法的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实例,在测试方法里alertsSchedulerService获得而后传递给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注入到userServiceuserDao属性中。支持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;


固然也能够在配置文件设置,这里就再也不介绍。

相关文章
相关标签/搜索