原文地址:
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);
}
}