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