【轉】JUnit 入門

原文地址: http://www.cnblogs.com/Peiyuan/articles/511494.html



測試的重要性毋庸再說,但如何使測試更加準確和全面,而且獨立於項目以外而且避免硬編碼, 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 單元的測試。

2、 JUnit 框架的結構
    經過前面的引子,其實咱們已經瞭解了 JUnit 基本的結構:
    1 import 聲明引入必須的 JUnit
    2 、定義一個測試類從 TestCase 繼承
    3 、必需一個調用 super( String) 的構造函數
    4 、測試類包含一些以 test .. 開頭的測試方法
    5 、每一個方法包含一個或者多個斷言語句
    固然還有一些其餘的內容,但知足以上幾條的就已是一個 JUnit 測試了

3、 JUnit 的命名規則和習慣
    1 、若是有一個名爲 ClassA 的被測試函數 , 那麼測試類的名稱就是 TestClassA
    2 、若是有一個名爲 methodA 的被測試函數,那麼測試函數的名稱就是 testMethodA

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...
        }        
    }

    上面這段代碼的執行順序是這樣的:
    1 oneTimeSetUp()
    2   setUp();
    3     testAccountAccess();
    4   tearDown();
    5   setUp();
    6     testEmployeeAccess();
    7   tearDown();
    8 oneTimeTearDown();

6、自定義 JUnit 斷言
    一般而言, JUnit 所提供的標準斷言對大多數測試已經足夠了。然而,在某些環境下,咱們可能更須要自定義一些斷言來知足咱們的須要。
    一般的作法是定義一個 TestCase 的子類,而且使用這個子類來知足全部的測試。新定義的共享的斷言或者公共代碼放到這個子類中。

7、測試代碼的放置
    三种放置方式 :
    1 、同一目錄——針對小型項目
      假設有一個項目類 , 名字爲
      com . peiyuan . business . Account
      相應的測試位於
      com . peiyuan . business . TestAccount
      即物理上存在於同一目錄
     
      優勢是 TestAccount 可以訪問 Account protected 成員變量和函數
      缺點是測試代碼處處都是,且堆積在產品代碼的目錄中

    2 、子目錄
      這個方案是在產品代碼的目錄之下建立一個 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;
          }
          ....
      }
 
    3 、並行樹
      把測試類和產品代碼放在同一個包中,但位於不一樣的源代碼樹,注意兩棵樹的根都在編譯器的 CLASSPATH 中。
      假設有一個項目類 , 位於
      prod / com . peiyuan . business . Account
      相應的測試位於
      test / com . peiyuan . business . TestAccount
     
      很顯然這種作法繼承了前兩種的優勢而摒棄了缺點,而且 test 代碼至關獨立

8、 Mock 的使用
    1 、基礎
      截至目前,前面提到的都是針對基本的 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 就實現了替身的做用。

    2 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());
  }

}
    3 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);
    }
}
相關文章
相關標籤/搜索