測試的重要性毋庸再說,但如何使測試更加準確和全面,而且獨立於項目以外而且避免硬編碼,JUnit給了咱們一個很好的解決方案。
1、引子
首先假設有一個項目類SimpleObject以下:
public class SimpleObject{
public List methodA(){
.....
}
}
其中定義了一個methodA方法返回一個對象,好,如今咱們要對這個方法進行測試,看他是否是返回一個List對象,是否是爲空,或者長度是否是符合標準等等。咱們寫這樣一個方法判斷返回對象是否不爲Null:
public void assertNotNull(Object object){
//判斷object是否不爲null
....
}
這個方法在JUnit框架中稱之爲一個斷言,JUnit提供給咱們了不少斷言,好比assertEqual,assertTrue...,咱們能夠利用這些斷言來判斷兩個值是否相等或者二元條件是否爲真等問題。
接下來咱們寫一個測試類
import junit.framework.*;
public class TestSimpleObject extends TestCase{
public TestSimpleObject(String name){
super(name);
}
public void testSimple(){
SimpleObject so=new SimpleObject();
assertNotNull(so.methodA());
}
}
而後咱們能夠運行JUnit來檢測咱們的測試結果,這樣咱們在不影響Project文件的前提下,實現了對Project單元的測試。html
2、JUnit框架的結構
經過前面的引子,其實咱們已經瞭解了JUnit基本的結構:
一、import聲明引入必須的JUnit類
二、定義一個測試類從TestCase繼承
三、必需一個調用super(String)的構造函數
四、測試類包含一些以test..開頭的測試方法
五、每一個方法包含一個或者多個斷言語句
固然還有一些其餘的內容,但知足以上幾條的就已是一個JUnit測試了java
3、JUnit的命名規則和習慣
一、若是有一個名爲ClassA的被測試函數,那麼測試類的名稱就是TestClassA
二、若是有一個名爲methodA的被測試函數,那麼測試函數的名稱就是testMethodAweb
4、JUnit自定義測試組合
在JUnit框相架下,他會自動執行全部以test..開頭的測試方法(利用java的反射機制),若是不想讓他這麼「智能」,一種方法咱們能夠改變測試方法的名稱,好比改爲pendingTestMethodA,這樣測試框架就會忽略它;第二種方法咱們能夠本身手工組合咱們須要的測試集合,這個魔力咱們能夠經過建立test suite來取得,任何測試類都可以包含一個名爲suite的靜態方法:
public static Test suite();
仍是以一個例子來講明,假設咱們有兩個名爲TestClassOne、TestClassTwo的測試類,以下:
import junit.framework.*;
public class TestClassOne extends TestCase{
public TestClassOne(String method){
super(method);
}
public void testAddition(){
assertEquals(4,2+2);
}
public void testSubtration(){
assertEquals(0,2-2);
}
}數據庫
import junit.framework.*;
public class TestClassTwo extends TestCase{
public TestClassTwo(String method){
super(method);
}
public void testLongest(){
ProjectClass pc=new ProjectClass();
assertEquals(100,pc.longest());
}
public void testShortest(){
ProjectClass pc=new ProjectClass();
assertEquals(1,pc.shortest(10));
}
public void testAnotherShortest(){
ProjectClass pc=new ProjectClass();
assertEquals(2,pc.shortest(5));
}
public static Test suite(){
TestSuite suite=new TestSuite();
//only include short tests
suite.addTest(new TestClassTwo("testShortest"));
suite.addTest(new TestClassTwo("testAnotherShortest"));
}
}
首先看TestClassTwo ,咱們經過suite顯式的說明了咱們要運行哪些test方法,並且,此時咱們看到了給構造函數的String參數是作什麼用的了:它讓TestCase返回一個對命名測試方法的引用。接下來再寫一個高一級別的測試來組合兩個測試類:
import junit.framework.*;
public class TestClassComposite extends TestCase{
public TestClassComposite(String method){
super(method);
}
static public Test suite(){
TestSuite suite = new TestSuite();
//Grab everything
suite.addTestSuite(TestClassOne.class);
//Use the suite method
suite.addTest(TestClassTwo.suite());
return suite;
}
}
組合後的測試類將執行TestClassOne中的全部測試方法和TestClassTwo中的suite中定義的測試方法。
5、JUnit中測試類的環境設定和測試方法的環境設定
每一個測試的運行都應該是互相獨立的;從而就能夠在任什麼時候候,以任意的順序運行每一個單獨的測試。
雖然這樣是有好處的,但咱們若是在每一個測試方法裏都寫上相同的設置和銷燬測試環境的代碼,那顯然是不可取的,好比取得數據庫聯接和關閉鏈接。好在JUnit的TestCase基類提供了兩個方法供咱們改寫,分別用於環境的創建和清理:
protected void setUp();
protected void tearDown();服務器
一樣道理,在某些狀況下,咱們須要爲整個test suite設置一些環境,以及在test suite中的全部方法都執行完成後作一些清理工做。要達到這種效果,咱們須要針對suite作一個setUp和tearDown,這可能稍微複雜一點,它須要提供所需的一個suite(不管經過什麼樣的方法)而且把它包裝進一個TestSetup對象網絡
看下面這個例子:
public class TestDB extends TestCase{
private Connection dbConn;
private String dbName;
private String dbPort;
private String dbUser;
private String dbPwd;
public TestDB(String method){
super(method);
}
//Runs before each test method
protected void setUp(){
dbConn=new Connection(dbName,dbPort,dbUser,dbPwd);
dbConn.connect();
}
//Runs after each test method
protected void tearDown(){
dbConn.disConnect();
dbConn=null;
}
public void testAccountAccess(){
//Uses dbConn
....
}
public void testEmployeeAccess(){
//Uses dbConn
....
}
public static Test suite(){
TestSuite suite=new TestSuite();
suite.addTest(new TestDB("testAccountAccess"));
suite.addTest(new TestDB("testEmployeeAccess"));
TestSetup wrapper=new TestSetup(suite){
protected void setUp(){
oneTimeSetUp();
}
protected void tearDown(){
oneTimeTearDown();
}
}
return wrapper;
}
//Runs at start of suite
public static void oneTimeSetUp(){
//load properties of initialization
//one-time initialize the dbName,dbPort...
}
//Runs at end of suite
public static void oneTimeTearDown(){
//one-time cleanup code goes here...
}
}app
上面這段代碼的執行順序是這樣的:
一、oneTimeSetUp()
二、 setUp();
三、 testAccountAccess();
四、 tearDown();
五、 setUp();
六、 testEmployeeAccess();
七、 tearDown();
八、oneTimeTearDown();框架
6、自定義JUnit斷言
一般而言,JUnit所提供的標準斷言對大多數測試已經足夠了。然而,在某些環境下,咱們可能更須要自定義一些斷言來知足咱們的須要。
一般的作法是定義一個TestCase的子類,而且使用這個子類來知足全部的測試。新定義的共享的斷言或者公共代碼放到這個子類中。函數
7、測試代碼的放置
三种放置方式:
一、同一目錄——針對小型項目
假設有一個項目類,名字爲
com.peiyuan.business.Account
相應的測試位於
com.peiyuan.business.TestAccount
即物理上存在於同一目錄
優勢是TestAccount可以訪問Account的protected成員變量和函數
缺點是測試代碼處處都是,且堆積在產品代碼的目錄中測試
二、子目錄
這個方案是在產品代碼的目錄之下建立一個test子目錄
同上,假設有一個項目類,名字爲
com.peiyuan.business.Account
相應的測試位於
com.peiyuan.business.test.TestAccount
優勢是能把測試代碼放遠一點,但又不置於太遠
缺點是測試代碼在不一樣的包中,因此測試類沒法訪問產品代碼中的protected成員,解決的辦法是寫一個產品代碼的子類來暴露那些成員。而後在測試代碼中使用子類。
舉一個例子,假設要測試的類是這樣的:
package com.peiyuan.business;
public class Pool{
protected Date lastCleaned;
....
}
爲了測試中得到non-public數據,咱們須要寫一個子類來暴露它
package com.peiyuan.business.test;
import com.peiyuan.business.Pool;
public class PoolForTesting extends Pool{
public Date getLastCleaned(){
return lastCleaned;
}
....
}
三、並行樹
把測試類和產品代碼放在同一個包中,但位於不一樣的源代碼樹,注意兩棵樹的根都在編譯器的CLASSPATH中。
假設有一個項目類,位於
prod/ com.peiyuan.business.Account
相應的測試位於
test/ com.peiyuan.business.TestAccount
很顯然這種作法繼承了前兩種的優勢而摒棄了缺點,而且test代碼至關獨立
8、Mock的使用
一、基礎
截至目前,前面提到的都是針對基本的java代碼的測試,可是假若遇到這樣的狀況:某個方法依賴於其餘一些難以操控的東西,諸如網絡、數據庫、甚至是servlet引擎,那麼在這種測試代碼依賴於系統的其餘部分,甚至依賴的部分還要再依賴其餘環節的狀況下,咱們最終可能會發現本身幾乎初始化了系統的每一個組件,而這只是爲了給某一個測試創造足夠的運行環境讓他能夠運行起來。這樣不只僅消耗了時間,還給測試過程引入了大量的耦合因素。
他的實質是一種替身的概念。
舉一個例子來看一下:假設咱們有一個項目接口和一個實現類。以下:
public interface Environmental{
public long getTime();
}
public class SystemEnvironment implements Environmental{
public long getTime(){
return System.currentTimeMillis();
}
}
再有一個業務類,其中有一個依賴於getTime的新方法
public class Checker{
Environmental env;
public Checker(Environmental anEnv){
env=anEnv;
}
public void reminder(){
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(env.getTime());
int hour =cal.get(Calendar.HOUR_OF_DAY);
if(hour>=17){
......
}
}
}
由上可見,reminder方法依賴於getTime爲他提供時間,程序邏輯實在下午5點以後進行提醒動做,但咱們作測試的時候不可能等到那個時候,因此就要寫一個假的Environmental來提供getTime方法,以下:
public class MockSystemEnvironment implements Environmental{
private long currentTime;
public long getTime(){
return currentTime;
}
public void setTime(long aTime){
currentTime= aTime;
}
}
寫測試的時候以這個類來替代SystemEnvironment就實現了替身的做用。
二、MockObject
接下來再看如何測試servlet,一樣咱們須要一個web服務器和一個servlet容器環境的替身,按照上面的邏輯,咱們須要實現HttpServletRequest和HttpServletResponse兩個接口。不幸的是一看接口,咱們有一大堆的方法要實現,呵呵,好在有人已經幫咱們完成了這個工做,這就是mockobjects對象。
import junit.framework.*;
import com.mockobjects.servlet.*;
public class TestTempServlet extends TestCase {
public void test_bad_parameter() throws Exception {
TemperatureServlet s = new TemperatureServlet();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
//在請求對象中設置參數
request.setupAddParameter( "Fahrenheit", "boo!");
//設置response的content type
response.setExpectedContentType( "text/html");
s.doGet(request,response);
//驗證是否響應
response.verify();
assertEquals("Invalid temperature: boo!\ n",
response.getOutputStreamContents());
}
public void test_boil() throws Exception {
TemperatureServlet s = new TemperatureServlet();
MockHttpServletRequest request =
new MockHttpServletRequest();
MockHttpServletResponse response =
new MockHttpServletResponse();
request.setupAddParameter( "Fahrenheit", "212");
response.setExpectedContentType( "text/html");
s.doGet(request,response);
response.verify();
assertEquals("Fahrenheit: 212, Celsius: 100.0\ n",
response.getOutputStreamContents());
}
}
三、EasyMock
EasyMock採用「記錄-----回放」的工做模式,基本使用步驟:
* 建立Mock對象的控制對象Control。
* 從控制對象中獲取所須要的Mock對象。
* 記錄測試方法中所使用到的方法和返回值。
* 設置Control對象到「回放」模式。
* 進行測試。
* 在測試完畢後,確認Mock對象已經執行了剛纔定義的全部操做
項目類:
package com.peiyuan.business;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>Title: 登錄處理</p>
* <p>Description: 業務類</p>
* <p>Copyright: Copyright (c) 2006</p>
* <p>Company: </p>
* @author Peiyuan
* @version 1.0
*/
public class LoginServlet extends HttpServlet {
/* (非 Javadoc)
* @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
// check username & password:
if("admin".equals(username) && "123456".equals(password)) {
ServletContext context = getServletContext();
RequestDispatcher dispatcher = context.getNamedDispatcher("dispatcher");
dispatcher.forward(request, response);
}
else {
throw new RuntimeException("Login failed.");
}
}
}
測試類:
package com.peiyuan.business;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import org.easymock.MockControl;
import junit.framework.TestCase;
/**
* <p>Title:LoginServlet測試類 </p>
* <p>Description: 基於easymock1.2</p>
* <p>Copyright: Copyright (c) 2006</p>
* <p>Company: </p>
* @author Peiyuan
* @version 1.0
*/
public class LoginServletTest extends TestCase {
/** * 測試登錄失敗的狀況 * @throws Exception */ public void testLoginFailed() throws Exception { //首先建立一個MockControl MockControl mc = MockControl.createControl(HttpServletRequest.class); //從控制對象中獲取所須要的Mock對象 HttpServletRequest request = (HttpServletRequest)mc.getMock(); //「錄製」Mock對象的預期行爲 //在LoginServlet中,前後調用了request.getParameter("username")和request.getParameter("password")兩個方法, //所以,須要在MockControl中設置這兩次調用後的指定返回值。 request.getParameter("username"); // 指望下面的測試將調用此方法,參數爲"username" mc.setReturnValue("admin", 1); // 指望返回值爲"admin",僅調用1次 request.getParameter("password"); // 指望下面的測試將調用此方法,參數爲" password" mc.setReturnValue("1234", 1); // 指望返回值爲"1234",僅調用1次 //調用mc.replay(),表示Mock對象「錄製」完畢 mc.replay(); //開始測試 LoginServlet servlet = new LoginServlet(); try { //因爲本次測試的目的是檢查當用戶名和口令驗證失敗後,LoginServlet是否會拋出RuntimeException, //所以,response對象對測試沒有影響,咱們不須要模擬它,僅僅傳入null便可。 servlet.doPost(request, null); fail("Not caught exception!"); } catch(RuntimeException re) { assertEquals("Login failed.", re.getMessage()); } // verify: mc.verify(); } /** * 測試登錄成功的狀況 * @throws Exception */ public void testLoginOK() throws Exception { //首先建立一個request的MockControl MockControl requestCtrl = MockControl.createControl(HttpServletRequest.class); //從控制對象中獲取所須要的request的Mock對象 HttpServletRequest requestObj = (HttpServletRequest)requestCtrl.getMock(); //建立一個ServletContext的MockControl MockControl contextCtrl = MockControl.createControl(ServletContext.class); //從控制對象中獲取所須要的ServletContext的Mock對象 final ServletContext contextObj = (ServletContext)contextCtrl.getMock(); //建立一個RequestDispatcher的MockControl MockControl dispatcherCtrl = MockControl.createControl(RequestDispatcher.class); //從控制對象中獲取所須要的RequestDispatcher的Mock對象 RequestDispatcher dispatcherObj = (RequestDispatcher)dispatcherCtrl.getMock(); requestObj.getParameter("username"); // 指望下面的測試將調用此方法,參數爲"username" requestCtrl.setReturnValue("admin", 1); // 指望返回值爲"admin",僅調用1次 requestObj.getParameter("password"); // 指望下面的測試將調用此方法,參數爲" password" requestCtrl.setReturnValue("123456", 1); // 指望返回值爲"1234",僅調用1次 contextObj.getNamedDispatcher("dispatcher"); contextCtrl.setReturnValue(dispatcherObj, 1); dispatcherObj.forward(requestObj, null); dispatcherCtrl.setVoidCallable(1); requestCtrl.replay(); contextCtrl.replay(); dispatcherCtrl.replay(); //爲了讓getServletContext()方法返回咱們建立的ServletContext Mock對象, //咱們定義一個匿名類並覆寫getServletContext()方法 LoginServlet servlet = new LoginServlet() { public ServletContext getServletContext() { return contextObj; } }; servlet.doPost(requestObj, null); }}