Easymock & DbUnit 入门介绍

          下面内容是昨天应甲方要求给项目组做的 Easymock 和 DbUnit 工具入门介绍及实践方面交流的文字部分。贴在这里一方面作以记录,另一方面为也有此需要的兄弟提供些素材。(我也参考&引用了不少,呵呵时间紧。)

 

单元测试与 mock 测试方法

          单元测试是对应用中的某一个模块(class)的功能(method)进行验证。在单元测试中,我们常遇到的问题是应用中其它的协同模块尚未开发完成,或者被测试模块需要和一些不容易构造、比较复杂的对象进行交互。由于不能肯定外围依赖模块的正确性,我们也无法确定测试中发现的问题是由哪个模块引起的。

 

stub

 

          Mock 对象能够模拟其它协同模块(class)的行为,被测试模块通过与 mock 对象协作,可以获得一个独立的测试环境。此外使用 mock 对象还可以模拟在应用中不容易构造(如 HttpServletRequest 必须在 Servlet 容器中才能构造出来)和比较复杂的对象(如 JDBC 中的 ResultSet 对象),从而使测试进行。

 

          Mock 测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟对象来代替真实对象以便测试的测试方法。

 

Mock object

          就是 mock 对象,模拟对象。它是真实对象在调试/测试期间的代替品。下面图中的 SUT (system under test) 表示被测试的对象,在测试过程中依赖 mock 对象。

 

Mock test

 

Mock 对象适用范畴

  1. 真实对象具有不可确定的行为,产生不可预测的效果,如采集的状态数据,天气预报等;
  2. 真实对象很难被创建;
  3. 真实对象的某些行为很难被触发;
  4. 真实对象还不存在,开发协作、进度、第三方外包、外围硬件等;

 

Mock 测试关键步骤

  1. 使用一个接口来描述这个对象(interface design);
  2. 在产品代码中实现这个接口(biz realize class);
  3. 在测试代码中实现这个接口(mock object class);
  4. 在测试代码使用mock object来模拟真实的业务对象。OO的实现隐藏。

 

主流 mock 测试工具

          手动的构造 mock object 会给开发人员带来额外的编码量,而且这些为创建 Mock  object 而编写的代码很有可能引入错误。目前有许多开源的mock工具项目,它们能够根据现有的接口或类动态生成 mock object。这样不仅能避免额外的编码工作,同时也降低了引入错误的可能。

 

          目前在 Java 阵营中主要的Mock测试工具有 EasyMock、JMock、MockCreator、Mockrunner 和 MockMaker等。在 .Net 阵营中主要是 Nmock和.NetMock 等。

 

EasyMock mock 测试工具

          EasyMock 是一套用于通过简单的方法对于给定的接口或类生成 Mock object 的类库。它提供对接口或类的模拟,能够通过录制、回放、检查三步来完成大体的测试过程。可以验证方法的调用、次数、顺序,可以令 mock object 返回指定的值或抛出指定异常。 通过 EasyMock我们可以方便的构造 mock object 从而使单元测试顺利进行。

 

Easymock logo

http://easymock.org/

 

          EasyMock provides Mock Objects for interfaces (and objects through the class extension) by generating them on the fly using Java's proxy mechanism. Due to EasyMock's unique style of recording expectations, most refactorings will not affect the Mock Objects. So EasyMock is a perfect fit for Test-Driven Development.

 

          EasyMock is open source software available under the terms of the MIT license.

 

EasyMock 单元测试步骤

          通过 EasyMock 我们可以为指定的接口或类动态的创建 mock object 来模拟依赖类。这个过程大致可以划分为以下几个步骤:

 

  1. 使用 EasyMock 生成 mock object;
  2. 设定 mock object 的预期行为和输出;(方法的调用、次数、顺序,返回值或抛出指定异常)
  3. 将 mock object 切换到 Replay 状态;
  4. 对产品代码中的类进行单元测试,其依赖 mock object (调用 mock object 方法);
  5. 对 mock object 的行为进行验证。

 

EasyMock 单元测试示例

          HttpServletRequest mock 示例。

 

import org.easymock.*;
import static org.easymock.EasyMock.*;

import junit.framework.*;

import javax.servlet.http.HttpServletRequest;

public class MockRequestTest extends TestCase {

	private IMocksControl control = EasyMock.createControl();

	private HttpServletRequest mockRequest;

	public MockRequestTest() {
		this.mockRequest = (HttpServletRequest) control
				.createMock(HttpServletRequest.class);
	}

	public void testMockRequest() {

		this.mockRequest.getParameter("name");
		expectLastCall().andReturn("name_value");

		this.control.replay();

		assertEquals("name_value", mockRequest.getParameter("name"));

		this.control.verify();
		
		
		this.control.reset();
		
		this.mockRequest.getParameter("name1");
		expectLastCall().andReturn("name_value1");

		this.control.replay();

		assertEquals("name_value1", mockRequest.getParameter("name1"));

		this.control.verify();
	}

	public void testMockRequestNullException() {

		this.mockRequest.getParameter(null);
		expectLastCall().andThrow(new NullPointerException());

		expect(this.mockRequest.getParameter("")).andThrow(new NullPointerException()).times(/*0 ,*/ 1);
		
		this.control.replay();

		try {
			assertEquals("name_value", mockRequest.getParameter(null));
			fail();
		} catch (NullPointerException e) {
			assertTrue(true);
		}

		try {
			assertEquals("name_value", mockRequest.getParameter(""));
			fail();
		} catch (NullPointerException e) {
			assertTrue(true);
		}

		this.control.verify();
	}
}

 

          1. javax.servlet.http.HttpServletRequest 接口。HttpServletRequest mock object 对该接口进行模拟。真正的 HttpServletRequest 对象需要在 Servlet 容器中构造。

 

          2. 一些简单的测试用例只需要一个mock object,可以用以org.easymock.EasyMock.createMock静态方法来创建:

 

static import org.easymock.EasyMock;

HttpServletRequest mockReq = createMock(HttpServletRequest.class);

 

          如果需要在相对复杂的测试用例中使用多个 mock object,EasyMock 提供了另外一种生成和管理 mock object的机制:

 

IMocksControl control = EasyMock.createControl();
HttpServletRequest mockReq = (HttpServletRequest) control.createMock(HttpServletRequest.class);

OtherInterface1 mockObj1 = (OtherInterface1) control.createMock(OtherInterface.class);

OtherInterface1 mockObj2 = (OtherInterface2) control.createMock(OtherInterface2.class);

 

          3. 如果您要模拟的是一个具体类而非接口,那么您需要使用 EasyMock Class Extension 包。在对具体类进行模拟时,您只要用 org.easymock.classextension.EasyMock 类中的静态方法代替 org.easymock.EasyMock 类中的静态方法即可。但存在限制不推荐。

 

EasyMock Class Extension (v2.4) Limitations
1. EasyMock Class Extension provides a built-in behavior for equals(), toString() and hashCode(). It means that you cannot record your own behavior for these methods. It is coherent with what EasyMock do. This limitation is considered to be a feature that prevents you from having to care about these methods.

2. Final methods cannot be mocked. If called, their normal code will be executed.

3. Private methods cannot be mocked. If called, their normal code will be executed. Remember this can occur during partial mocking.

http://easymock.org/EasyMock2_4_ClassExtension_Documentation.html

 

          4. 设定 mock object 的预期行为和输出。一个 mock object 会经历两个状态 Record 和 Replay 状态。Mock object 创建后初始状态被设置为 Record,该状态允许程序设定 mock object 的预期行为和输出。

 

          5. 添加 mock object 行为的过程通常可以分为以下 3 步:


              a) 对 mock object 的特定方法作出调用;


              b) 通过 EasyMock.expectLastCall 静态方法获取上一次方法调用所对应的 IExpectationSetters 实例并设定预期输出,包括返回结果值、抛出异常、调用次数和顺序。

 

          6. 将 mock object 切换到 Replay 状态。在使用 mock object 进行实际的测试前,我们需要将 mock object 的状态切换为 Replay。在 Replay 状态时 mock object 能够根据设定对特定的方法调用作出预期的响应。将 mock object 切换成 Replay 状态有两种方式,根据生成方式进行选择。如果是通过 org.easymock.EasyMock.createMock 静态方法生成的 mock object,那么 EasyMock 类提供了相应的 replay 静态方法用于将 mock object 切换为 Replay 状态; 如果 mock object 是通过 IMocksControl 接口提供的 createMock 方法生成的,那么通过 IMocksControl 接口的 replay 方法对它所创建的所有 mock object 进行切换。

 

          7. 对 mock object 的行为进行验证。

 

verify(mockObj);

control.verify();

 

单元测试与 DbUnit 工具

          在对涉及到数据处理(DAO)的模块(class)进行单元测试中,由于其往往依赖于数据状态,因此为这些代码编写单元测试是一件很不轻松的工作。在这种情况下,要想进行有效的单元就必须隔离测试对象和外部依赖数据,管理测试对象的状态和行为。

 

          开源的 DbUnit 项目为以上述问题提供了一个优雅的解决方案。通过 DbUnit 工具开发人员可以控制测试数据库的状态。在进行一个 DAO 单元测试之前,DbUnit 为数据库准备好初始数据,而在测试结束后 DbUnit 会把数据库状态恢复到测试前的状态。

 

DbUnit logo

http://www.dbunit.org/

 

          DbUnit is a JUnit extension (also usable with Ant) targeted at database-driven projects that, among other things, puts your database into a known state between test runs. This is an excellent way to avoid the myriad of problems that can occur when one test case corrupts the database and causes subsequent tests to fail or exacerbate the damage. DbUnit has the ability to export and import your database data to and from XML datasets. Since version 2.0, DbUnit can also work with very large datasets when used in streaming mode. DbUnit can also help you to verify that your database data match an expected set of values.

 

          DbUnit is open source software available under the GNU Lesser GPL license.

          DBUnit 因为具有 XML 与数据库双向映射的功能,而且支持多种主流数据库(数据类型)。

 

DbUnit 单元测试示例

public class TestDao extends DBTestCase {

	public TestDao() {
		super("TestDao");
		System.setProperty(
			PropertiesBasedJdbcDatabaseTester.DBUNIT_DRIVER_CLASS,
				"oracle.jdbc.driver.OracleDriver");
		System.setProperty(							PropertiesBasedJdbcDatabaseTester.DBUNIT_CONNECTION_URL,
				"jdbc:oracle:thin:@127.0.0.1:1521:ORACLE");
		System.setProperty(PropertiesBasedJdbcDatabaseTester.DBUNIT_USERNAME, "local");
		System.setProperty(PropertiesBasedJdbcDatabaseTester.DBUNIT_PASSWORD, "local");

		// Note that for Oracle you must specify the schema name in uppercase.
	System.setProperty(PropertiesBasedJdbcDatabaseTester.DBUNIT_SCHEMA, "LOCAL");
	}

	@Override
	protected void setUpDatabaseConfig(DatabaseConfig config) {
		// Enable the qualified table names feature for oracle.
		config.setFeature(DatabaseConfig.FEATURE_QUALIFIED_TABLE_NAMES, true);
		// Skip Oracle 10g Recyclebin tables.
		config.setFeature(DatabaseConfig.FEATURE_SKIP_ORACLE_RECYCLEBIN_TABLES, true);
		
		// Setup oracle 10g data type factory. 
		config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new OracleDataTypeFactory());
	}

	@Override
	protected IDataSet getDataSet() throws Exception {
		return new FlatXmlDataSet(new FileInputStream("people_flag1.xml"));
	}

	/***
	 * DatabaseOperation
	 * http://www.dbunit.org/components.html
	 */
	@Override
	protected DatabaseOperation getSetUpOperation() throws Exception {
		
		// This operation literally refreshes dataset contents into the target database.
		// return DatabaseOperation.REFRESH;

		// This composite operation performs a DELETE_ALL operation followed by an INSERT operation.
		return DatabaseOperation.CLEAN_INSERT;
	}

	@Override
	protected DatabaseOperation getTearDownOperation() throws Exception {

		// Empty operation that does absolutely nothing.
		return DatabaseOperation.NONE;

		// Deletes all rows of tables present in the specified dataset.
		// return DatabaseOperation.DELETE_ALL;
	}

	public void testPeopleTableReady() throws Exception {
		IDataSet dbDataSet = getConnection().createDataSet();  
		ITable dbTable = dbDataSet.getTable("LOCAL.PEOPLE");  

		IDataSet xmlDataSet = new FlatXmlDataSet(new FileInputStream("people_flag1.xml"));  
		ITable xmlTable = xmlDataSet.getTable("LOCAL.PEOPLE");  

		Assertion.assertEquals(xmlTable, dbTable);
	}
	
	public void testPersonDaoSave() throws Exception {
		
		Person p = new Person();
		
		p.setId(6);
		p.setName("testUserA");
		p.setPassword("testPasswordA");
		p.setFlag(1);
		
		PersonDao pd = new PersonDao();
		
		Person res = pd.save(p);
		
		IDataSet dbDataSet = getConnection().createDataSet();  
		ITable dbTable = dbDataSet.getTable("LOCAL.PEOPLE");  

		IDataSet xmlDataSet = new FlatXmlDataSet(new FileInputStream("expected_people_save.xml"));  
		ITable expectedTable = xmlDataSet.getTable("LOCAL.PEOPLE");  

		Assertion.assertEquals(expectedTable, dbTable);
		
		assertEquals(p, res);
	}

	public void testPersonDaoUpdate() throws Exception {
		
		Person np = new Person();
		
		np.setId(6);
		np.setName("testUserA1");
		np.setPassword("testPasswordA1");
		np.setFlag(0);
		
		Person op = new Person();
		
		op.setId(5);
		
		PersonDao pd = new PersonDao();
		
		Person res = pd.Update(op, np);
		
		IDataSet dbDataSet = getConnection().createDataSet();  
		ITable dbTable = dbDataSet.getTable("LOCAL.PEOPLE");  

		IDataSet xmlDataSet = new FlatXmlDataSet(new FileInputStream("expected_people_update.xml"));  
		ITable expectedTable = xmlDataSet.getTable("LOCAL.PEOPLE");  

		Assertion.assertEquals(expectedTable, dbTable);
		
		assertEquals(np, res);
	}
}

 

package com.javaeye.lzy.dao;

public class Person {

	private long id = 0;
	private String name = null;
	private String password = null;
	private int flag = 0;

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public int getFlag() {
		return flag;
	}

	public void setFlag(int flag) {
		this.flag = flag;
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + flag;
		result = prime * result + (int) (id ^ (id >>> 32));
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		result = prime * result
				+ ((password == null) ? 0 : password.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Person other = (Person) obj;
		if (flag != other.flag)
			return false;
		if (id != other.id)
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		if (password == null) {
			if (other.password != null)
				return false;
		} else if (!password.equals(other.password))
			return false;
		return true;
	}
}

 

package com.javaeye.lzy.dao;

import java.sql.Connection;
import java.sql.Statement;

public class PersonDao {
	
	public Person save(Person p) throws Exception
	{
		if (p == null)
			return null;
		
		Connection conn = ConnectionManager.getInstance().getConnection();

		try {
			Statement stmt = conn.createStatement();
			
			stmt.execute("INSERT INTO LOCAL.PEOPLE(ID, NAME, PASSWORD, FLAG)" +
					"VALUES(" + p.getId() + ", '" + p.getName() + "', '" + p.getPassword() + "', " + p.getFlag() + ")");
		}
		finally
		{
			conn.close();
		}
		
		return p;
	}

	public Person Update(Person op, Person np) throws Exception
	{
		if (op == null || np == null)
			return null;
		
		Connection conn = ConnectionManager.getInstance().getConnection();

		try {
			Statement stmt = conn.createStatement();
			
			stmt.execute("UPDATE LOCAL.PEOPLE " +
					"SET ID = " + np.getId() + ", NAME = '" + np.getName() + "', PASSWORD = '" + np.getPassword() + "', FLAG = " + np.getFlag() +
					" WHERE ID = " + op.getId());
		}
		finally
		{
			conn.close();
		}
		
		return np;
	}
}

 

package com.javaeye.lzy;

import java.io.FileOutputStream;
import java.sql.Connection;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.xml.FlatDtdDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.ext.oracle.Oracle10DataTypeFactory;

import com.javaeye.lzy.dao.ConnectionManager;

public class DbUnitSampleDataSetExportApp {

	public static void main(String[] args) throws Exception {

		Connection conn = ConnectionManager.getInstance().getConnection();

		try {

			IDatabaseConnection dbconn = new DatabaseConnection(conn, "LOCAL");
			dbconn.getConfig().setFeature("http://www.dbunit.org/features/qualifiedTableNames", true);
			dbconn.getConfig().setFeature("http://www.dbunit.org/features/skipOracleRecycleBinTables", true);
			dbconn.getConfig().setProperty("http://www.dbunit.org/properties/datatypeFactory", new Oracle10DataTypeFactory());

			QueryDataSet dataSet = new QueryDataSet(dbconn);
			dataSet.addTable("LOCAL.PEOPLE",
					"SELECT * FROM PEOPLE WHERE FLAG = 1 ORDER BY ID"
					// "SELECT * FROM PEOPLE ORDER BY ID"
					);

			FlatXmlDataSet.write(dataSet, new FileOutputStream("people_flag1.xml"));
			// FlatXmlDataSet.write(dataSet, new FileOutputStream("people_all.xml"));
			FlatDtdDataSet.write(dataSet, new FileOutputStream("people.dtd"));

			// org.dbunit.dataset.IDataSet dataSet = dbconn.createDataSet();
			//
			// FlatXmlDataSet.write(dataSet, new FileOutputStream("db.xml"));
			// FlatDtdDataSet.write(dataSet, new FileOutputStream("db.dtd"));
		} finally {
			conn.close();
		}
	}
}

 

Dbunit 最佳实践

          原文:http://dbunit.sourceforge.net/bestpractices.html

 

          Best Practices

  1. Use one database instance per developer.
  2. Good setup don't need cleanup!
  3. Use multiple small datasets.
  4. Perform setup of stale data once for entire test class or test suite.
  5. Connection management strategies.

 

  • 每个开发人员使用一个数据库

          让你的数据库在测试运行之前处于一个已知状态可以简化测试。一个数据库在同一时间应该只用于一个测试,否则数据库状态无法保障。所以同一项目的多个开发人员应该有每人一个数据库,这样可以防止数据紊乱,这也可以简化数据清除,你不必在每次测试前将数据库回滚到其初始状态。

 

  • 好的 setup 无需清除数据

          你应该始终避免产生依赖以前测试结果的测试,幸好这也是 dbunit 主要目标。原则上如果你使用“每个开发人员一个数据”的实践,不要害怕在测试之后留下你的数据。如果你在测试运行之前将数据库置于一个已知状态,那么你无需清除数据。这可以简化测试维护和减少清除操作带来的开销。有时候如果测试失败,这有助于你手工检测数据库。

 

  • 使用多个小数据集

          你的大部分测试不需要整个数据库每次测试都重新初始化。所以在一个大型数据库的应用中无需为整个数据库准备数据集,你应该将整个数据库的数据集拆成一块块的小数据集。这些小块的数据集能大致对应你的逻辑单元或者说组件。这减少了每次测试都初始化数据库的开销。这对小组开发也极为有利,因为工作于不同组件的开发者可以独立的修改数据集。
对集成测试来说,你仍然可以使用 CompositeDataSet 类在运行时候将多个小数据集绑定成一个大的,只为整个测试或数据集 setup 一次不变数据。如果多个测试使用相同的只读数据,那么整个测试类或测试集可以只初始化这些数据一次。你必须小心确保你从不会修改这些数据。这也能减少运行测试的时间,但也引入更多风险。

 

  • 连接管理策略

          以下是推荐的连接管理策略,分为远程测试和容器内测试两类:

            a) 使用 DatabaseTestCase 的远程客户。你应该尝试为整个测试集复用同一个连接,这样可以减少每次测试获取新连接的开销。从 1.1 版以来 DatabaseTestCase 在 setUp() 和 tearDown() 方法中都会关闭连接。你可以覆盖 closeConnection() 方法,在方法体无需写任何代码就可以避免关闭连接。


            b) 容器内使用 Cactus or JUnitEE 做测试。如果你使用容器内测试策略,那么你应该使用 DatabaseDataSourceConnection 类访问你在应用服务器配置的数据源。你也可以从数据源获取 JDBC 连接。所以你能依赖应用服务器内建的连接池获取更好的性能。类似如下代码:

 

IDatabaseConnection connection = new DatabaseDataSourceConnection(new InitialContext(), "jdbc/myDataSource");

 

          就是以上这些,很基础很入门不过用起来还是很简单的。附件“dbunit_samples.zip”是上面 DbUnit 示例的完整版本,注释中的连接多少会有些用。另外想说的是,单元测试本身就应该很简单,扁平&实用,而且应该少想些多做些,单元测试自然就会用起来并发挥它的作用,最终提高代码&产品质量。

 

作者:lzy.je
出处:http://lzy.iteye.com
本文版权归作者所有,只允许以摘要和完整全文两种形式转载,不允许对文字进行裁剪。未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

 

 

你可能感兴趣的:(DAO,oracle,xml,jdbc,单元测试)