单元测试:Unitils的简单使用

jopen 7年前

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

}

也许通过判断两个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;


当然也可以在配置文件设置,这里就不再介绍。