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
本文版權歸作者所有,只允許以摘要和完整全文兩種形式轉載,不允許對文字進行裁剪。未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章