??xml version="1.0" encoding="utf-8" standalone="yes"?> 毋庸|疑Q程序员要对自己~写的代码负责,您不仅要保证它能通过~译Q正常地q行Q而且要满需求和设计预期的效果。单元测试正是验证代码行为是否满预期的有效手段之一。但不可否认Q做试是g很枯燥无的事情Q而一遍又一遍的试则更是让人生畏的工作。幸q的是,单元试工具 JUnit 使这一切变得简单艺术v来?/p>
JUnit ?Java C中知名度最高的单元试工具。它诞生?1997 q_?Erich Gamma ?Kent Beck 共同开发完成。其?Erich Gamma 是经典著作《设计模式:可复用面向对象Y件的基础》一书的作者之一Qƈ?Eclipse 中有很大的A献;Kent Beck 则是一位极限编E(XPQ方面的专家和先驱?/p>
麻雀虽小Q五脏俱全。JUnit 设计的非常小巧,但是功能却非常强大。Martin Fowler 如此评h JUnitQ在软g开发领域,从来没有如此少的代码vC如此重要的作用。它大大化了开发h员执行单元测试的隑ֺQ特别是 JUnit 4 使用 Java 5 中的注解QannotationQɋ试变得更加单?/p>
在开始体?JUnit 4 之前Q我们需要以下Y件的支持Q?/p>
首先为我们的体验新徏一?Java 工程 —?coolJUnit。现在需要做的是Q打开目 coolJUnit 的属性页 -> 选择“Java Build Path”子选项 -> 炚w“Add Library…”按?-> 在弹出的“Add Library”对话框中选择 JUnitQ?a >?Q,q在下一中选择版本 4.1 后点几ZFinish”按钮。这样便?JUnit 引入到当前项目库中了?/p>
JUnit 4.1 是基?Java 5 的升U版本,它用了 Tiger 中的很多新特性来化原有的使用方式。正因ؓ如此Q它q不能直接运行在 JDK1.4.x 版本上。如果您需要在 JDK1.4.x 版本使用 JUnit 的话Q请使用 3.8.1 版本?/p>
可以开始编写单元测试了吗?{等……,您打把单元试代码攑֜什么地方呢Q把它和被测试代码在一Pq显然会照成混ؕQ因为单元测试代码是不会出现在最l品中的。徏议您分别为单元测试代码与被测试代码创建单独的目录Qƈ保证试代码和被试代码使用相同的包名。这h保证了代码的分离Q同时还保证了查扄方便。遵照这条原则,我们在项?coolJUnit 根目录下d一个新目录 testsrcQƈ把它加入到项目源代码目录中(加入方式??Q?/p>
现在我们得到了一?JUnit 的最佛_践:单元试代码和被试代码使用一L包,不同的目录?/p>
一切准备就l,一起开始体验如何?JUnit q行单元试吧。下面的例子来自W者的开发实践:工具c?WordDealUtil 中的静态方?wordFormat4DB 是专用于处理 Java 对象名称向数据库表名转换的方法(您可以在代码注释中可以得到更多详l的内容Q。下面是W一ơ编码完成后大致情ŞQ?/p>
它是否能按照预期的效果执行呢Q尝试ؓ它编?JUnit 单元试代码如下Q?/p>
很普通的一个类嘛!试c?TestWordDealUtil 之所以用“Test”开_完全是ؓ了更好的区分试cM被测试类。测试方?wordFormat4DBNormal 调用执行被测试方?WordDealUtil.wordFormat4DBQ以判断q行l果是否辑ֈ设计预期的效果。需要注意的是,试Ҏ wordFormat4DBNormal 需要按照一定的规范书写Q?/p>
试Ҏ中要处理的字W串为“employeeInfo”,按照设计目的Q处理后的结果应该ؓ“employee_info”。assertEquals 是由 JUnit 提供的一pd判断试l果是否正确的静态断aҎQ位于类 org.junit.Assert 中)之一Q我们用它执行结?result 和预期值“employee_info”进行比较,来判断测试是否成功?/p>
看看q行l果如何。在试cM点击右键Q在弹出菜单中选择 Run As JUnit Test。运行结果如下图所C: l色的进度条提示我们Q测试运行通过了。但现在宣布代码通过了单元测试还为时q早。记住:您的单元试代码不是用来证明您是对的Q而是Z证明您没有错。因此单元测试的范围要全面,比如对边界倹{正常倹{错误值得试Q对代码可能出现的问题要全面预测Q而这也正是需求分析、详l设计环节中要考虑的。显Ӟ我们的测试才刚刚开始,l箋补充一些对Ҏ情况的测试: 再次q行试。很遗憾QJUnit q行界面提示我们有两个测试情冉|通过试Q?a >?Q——当首字母大写时得到的处理结果与预期的有偏差Q造成试p|QfailureQ;而当试?null 的处理结果时Q则直接抛出了异常——测试错误(errorQ。显Ӟ被测试代码中q没有对首字母大写和 null q两U特D情况进行处理,修改如下Q?/p>
JUnit 测试失败的情况分ؓ两种Qfailure ?error。Failure 一般由单元试使用的断aҎ判断p|引vQ它表示在测试点发现了问题;?error 则是׃码异常引Pq是试目的之外的发玎ͼ它可能生于试代码本n的错误(试代码也是代码Q同h法保证完全没有缺PQ也可能是被试代码中的一个隐藏的bug?/p>
L记这一?JUnit 最佛_践:试M可能的错误。单元测试不是用来证明您是对的,而是Z证明您没有错?/p>
啊哈Q再ơ运行测试,l条又重现眼前。通过?WordDealUtil.wordFormat4DB 比较全面的单元测试,现在的代码已l比较稳定,可以作ؓ API 的一部分提供l其它模块用了?/p>
不知不觉中我们已l?JUnit 漂亮的完成了一ơ单元测试。可以体会到 JUnit 是多么轻量Q多么简单,Ҏ不需要花心思去研究Q这可以把更多的注意力攑֜更有意义的事情上——编写完整全面的单元试?/p>
当然QJUnit 提供的功能决不仅仅如此简单,在接下来的内容中Q我们会看到 JUnit 中很多有用的Ҏ,掌握它们Ҏ灉|的编写单元测试代码非常有帮助?/p>
何谓 FixtureQ它是指在执行一个或者多个测试方法时需要的一pd公共资源或者数据,例如试环境Q测试数据等{。在~写单元试的过E中Q您会发现在大部分的试Ҏ在进行真正的试之前都需要做大量的铺垫——ؓ设计准备 Fixture 而忙。这些铺垫过E占据的代码往往比真正测试的代码多得多,而且q个比率随着试的复杂程度的增加而递增。当多个试Ҏ都需要做同样的铺垫时Q重复代码的“坏味道”便在测试代码中弥O开来。这股“坏味道”会弄脏您的代码Q还会因为疏忽造成错误Q应该用一些手D|栚w它?/p>
JUnit 专门提供了设|公?Fixture 的方法,同一试cM的所有测试方法都可以q它来初始?Fixture 和注销 Fixture。和~写 JUnit 试Ҏ一P公共 Fixture 的设|也很简单,您只需要: 遵@上面的三条原则,~写出的代码大体是这个样子: q样Q在每一个测试方法执行之前,JUnit 会保?init Ҏ已经提前初始化测试环境,而当此测试方法执行完毕之后,JUnit 又会调用 destroy Ҏ注销试环境。注意是每一个测试方法的执行都会触发对公?Fixture 的设|,也就是说使用注解 Before 或?After 修饰的公?Fixture 讄Ҏ是方法别的Q?a >?Q。这样便可以保证各个独立的测试之间互不干扎ͼ以免其它试代码修改试环境或者测试数据媄响到其它试代码的准性?/p>
可是Q这U?Fixture 讄方式q是引来了批评,因ؓ它效率低下,特别是在讄 Fixture 非常耗时的情况下Q例如设|数据库链接Q。而且对于不会发生变化的测试环境或者测试数据来_是不会媄响到试Ҏ的执行结果的Q也没有必要针Ҏ一个测试方法重新设|一?Fixture。因此在 JUnit 4 中引入了cȝ别的 Fixture 讄ҎQ编写规范如下: cȝ别的 Fixture 仅会在测试类中所有测试方法执行之前执行初始化Qƈ在全部测试方法测试完毕之后执行注销ҎQ?a >?Q。代码范本如下: 注解 org.junit.Test 中有两个非常有用的参敎ͼexpected ?timeout。参?expected 代表试Ҏ期望抛出指定的异常,如果q行试q没有抛个异常,?JUnit 会认个测试没有通过。这为验证被试Ҏ在错误的情况下是否会抛出预定的异常提供了便利。D例来_Ҏ supportDBChecker 用于查用户用的数据库版本是否在pȝ的支持的范围之内Q如果用户用了不被支持的数据库版本Q则会抛行时异常 UnsupportedDBVersionException。测试方?supportDBChecker 在数据库版本不支持时是否会抛出指定异常的单元试Ҏ大体如下Q?/p>
注解 org.junit.Test 的另一个参?timeoutQ指定被试Ҏ被允许运行的最长时间应该是多少Q如果测试方法运行时间超q了指定的毫U数Q则JUnit认ؓ试p|。这个参数对于性能试有一定的帮助。例如,如果解析一份自定义?XML 文档p了多?1 U的旉Q就需要重新考虑 XML l构的设计,那单元测试方法可以这h写: JUnit 提供注解 org.junit.Ignore 用于暂时忽略某个试ҎQ因为有时候由于测试环境受限,q不能保证每一个测试方法都能正运行。例如下面的代码便表C由于没有了数据库链接,提示 JUnit 忽略试Ҏ unsupportedDBCheckQ?/p>
但是一定要心。注?org.junit.Ignore 只能用于暂时的忽略测试,如果需要永q忽略这些测试,一定要认被测试代码不再需要这些测试方法,以免忽略必要的测试点?/p>
又一个新概念出现了——测试运行器QJUnit 中所有的试Ҏ都是由它负责执行的。JUnit 为单元测试提供了默认的测试运行器Q但 JUnit q没有限制您必须使用默认的运行器。相反,您不仅可以定制自qq行器(所有的q行器都l承?org.junit.runner.RunnerQ,而且q可以ؓ每一个测试类指定使用某个具体的运行器。指定方法也很简单,使用注解 org.junit.runner.RunWith 在测试类上显式的声明要用的q行器即可: 显而易见,如果试cL有显式的声明使用哪一个测试运行器QJUnit 会启动默认的试q行器执行测试类Q比如上面提及的单元试代码Q。一般情况下Q默认测试运行器可以应对l大多数的单元测试要求;当?JUnit 提供的一些高U特性(例如卛_介绍的两个特性)或者针对特D需求定?JUnit 试方式Ӟ昑ּ的声明测试运行器必不可了?/p>
在实际项目中Q随着目q度的开展,单元试cM来多Q可是直到现在我们还只会一个一个的单独q行试c,q在实际目实践中肯定是不可行的。ؓ了解册个问题,JUnit 提供了一U批量运行测试类的方法,叫做试套g。这P每次需要验证系l功能正性时Q只执行一个或几个试套g便可以了。测试套件的写法非常单,您只需要遵循以下规则: 上例代码中,我们前文提到的试c?TestWordDealUtil 攑օ了测试套?RunAllUtilTestsSuite 中,?Eclipse 中运行测试套Ӟ可以看到试c?TestWordDealUtil 被调用执行了。测试套件中不仅可以包含基本的测试类Q而且可以包含其它的测试套Ӟq样可以很方便的分层理不同模块的单元测试代码。但是,您一定要保证试套g之间没有循环包含关系Q否则无的循环׃出现在您的面前……?/p>
回顾一下我们在节?a >JUnit 初体?/font>”中丄实例。ؓ了保证单元测试的严}性,我们模拟了不同类型的字符串来试Ҏ的处理能力,为此我们~写大量的单元测试方法。可是这些测试方法都是大同小异:代码l构都是相同的,不同的仅仅是试数据和期望倹{有没有更好的方法将试Ҏ中相同的代码l构提取出来Q提高代码的重用度,减少复制_脓代码的烦|在以前的 JUnit 版本上,q没有好的解x法,而现在您可以使用 JUnit 提供的参数化试方式应对q个问题?/p>
参数化测试的~写E微有点ȝQ当然这是相对于 JUnit 中其它特性而言Q: 我们按照q个标准Q重新改造一番我们的单元试代码Q?/p>
很明显,代码瘦n了。在静态方?words 中,我们使用二维数组来构建测试所需要的参数列表Q其中每个数l中的元素的攄序q没有什么要求,只要和构造函C的顺序保持一致就可以了。现在如果再增加一U测试情况,只需要在静态方?words 中添加相应的数组卛_Q不再需要复制粘贴出一个新的方法出来了?/p>
随着目的进展,目的规模在不断的膨胀Qؓ了保证项目的质量Q有计划的执行全面的单元试是非常有必要的。但单靠JUnit提供的测试套件很难胜任这工作,因ؓ目中单元测试类的个数在不停的增加,试套g却无法动态的识别新加入的单元试c,需要手动修Ҏ试套Ӟq是一个很Ҏ遗忘得步骤,E有疏忽׃影响全面单元试的覆盖率?/p>
当然解决的方法有多种多样Q其中将 JUnit 与构建利?Ant l合使用可以很简单的解决q个问题。Ant —?备受赞誉?Java 构徏工具。它凭借出色的易用性、^台无x以及对目自动试和自动部|的支持Q成Z多项目构E中不可或缺的独立工Pq已l成Z实上的标准。Ant 内置了对 JUnit 的支持,它提供了两个 TaskQjunit ?junitreportQ分别用于执?JUnit 单元试和生成测试结果报告。用这两个 Task ~写构徏脚本Q可以很单的完成每次全面单元试的Q务? 不过Q在使用 Ant q行 JUnit 之前Q您需要稍作一些配|。打开 Eclipse 首选项界面Q选择 Ant -> Runtime 首选项Q见?Q,?JUnit 4.1 ?JAR 文gd?Classpath Tab 中?Global Entries 讄w。记得检查一?Ant Home Entries 讄中?Ant 版本是否?1.7.0 之上Q如果不是请替换为最新版本的 Ant JAR 文g?/p>
剩下的工作就是要~写 Ant 构徏脚本 build.xml。虽然这个过E稍嫌繁琐,但这是一件一x逸的事情。现在我们就把前面编写的试用例都放|到 Ant 构徏脚本中执行,为项?coolJUnit 的构本添加一下内容: Target junit report ?Ant 构徏脚本中的核心内容Q其?target 都是为它的执行提供前期服务。Task junit 会寻找输出目录下所有命名以“Test”开头的 class 文gQƈ执行它们。紧接着 Task junitreport 会将执行l果生成 HTML 格式的测试报告(?Q放|在“report folder”下?/p>
为整个项目的单元试cȝ定一U命名风根{不仅是Z区分cd的考虑Q这?Ant 扚w执行单元试也非常有帮助Q比如前面例子中的测试类都已“Test”打_而测试套件则以“Suite”结{?/p>
现在执行一ơ全面的单元试变得非常单了Q只需要运行一?Ant 构徏脚本Q就可以走完所有流E,q能得到一份详的试报告。您可以?Ant 在线手册 中获得上面提及的每一?Ant 内置 task 的用细节?/p>
随着来多的开发h员开始认同ƈ接受极限~程QXPQ的思想Q单元测试的作用在Y件工E中变得来重要。本文旨在将最新的单元试工具 JUnit 4 介绍l您Q以及如何结?IDE Eclipse 和构建工?Ant 创徏自动化单元测试方案。ƈ且还期望您能够通过本文“感染”一些好的单元测试意识,因ؓ JUnit 本n仅仅是一份工兯已Q它的真正优势来自于它的思想和技术?/p>
回页?/font>
? 为项目添?JUnit ?/b>
h?JDK 的版?/b>
? 修改目源代码目?/b>
package com.ai92.cooljunit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 对名U、地址{字W串格式的内容进行格式检?
* 或者格式化的工L
*
* @author Ai92
*/
public class WordDealUtil {
/**
* Java对象名称Q每个单词的头字母大写)按照
* 数据库命名的习惯q行格式?
* 格式化后的数据ؓ写字母Qƈ且用下划线分割命名单词
*
* 例如QemployeeInfo l过格式化之后变?employee_info
*
* @param name Java对象名称
*/
public static String wordFormat4DB(String name){
Pattern p = Pattern.compile("[A-Z]");
Matcher m = p.matcher(name);
StringBuffer sb = new StringBuffer();
while(m.find()){
m.appendReplacement(sb, "_"+m.group());
}
return m.appendTail(sb).toString().toLowerCase();
}
}
package com.ai92.cooljunit;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class TestWordDealUtil {
//试wordFormat4DB正常q行的情?
@Test public void wordFormat4DBNormal(){
String target = "employeeInfo";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("employee_info", result);
}
}
? JUnit q行成功界面
public class TestWordDealUtil {
…?
//试 null 时的处理情况
@Test public void wordFormat4DBNull(){
String target = null;
String result = WordDealUtil.wordFormat4DB(target);
assertNull(result);
}
//试I字W串的处理情?
@Test public void wordFormat4DBEmpty(){
String target = "";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("", result);
}
//试当首字母大写时的情况
@Test public void wordFormat4DBegin(){
String target = "EmployeeInfo";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("employee_info", result);
}
//试当尾字母为大写时的情?
@Test public void wordFormat4DBEnd(){
String target = "employeeInfoA";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("employee_info_a", result);
}
//试多个相连字母大写时的情况
@Test public void wordFormat4DBTogether(){
String target = "employeeAInfo";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("employee_a_info", result);
}
}
//修改后的ҎwordFormat4DB
/**
* Java对象名称Q每个单词的头字母大写)按照
* 数据库命名的习惯q行格式?
* 格式化后的数据ؓ写字母Qƈ且用下划线分割命名单词
* 如果参数name为nullQ则q回null
*
* 例如QemployeeInfo l过格式化之后变?employee_info
*
* @param name Java对象名称
*/
public static String wordFormat4DB(String name){
if(name == null){
return null;
}
Pattern p = Pattern.compile("[A-Z]");
Matcher m = p.matcher(name);
StringBuffer sb = new StringBuffer();
while(m.find()){
if(m.start() != 0)
m.appendReplacement(sb, ("_"+m.group()).toLowerCase());
}
return m.appendTail(sb).toString().toLowerCase();
}
? JUnit q行p|界面
L讎ͼ
回页?/font>
//初始化FixtureҎ
@Before public void init(){……}
//注销FixtureҎ
@After public void destroy(){……}
? ҎU别 Fixture 执行C意?/b>
//cȝ别Fixture初始化方?
@BeforeClass public static void dbInit(){……}
//cȝ别Fixture注销Ҏ
@AfterClass public static void dbClose(){……}
? cȝ?Fixture 执行C意?/b>
@Test(expected=UnsupportedDBVersionException.class)
public void unsupportedDBCheck(){
…?
}
@Test(timeout=1000)
public void selfXMLReader(){
…?
}
@ Ignore(“db is down?
@Test(expected=UnsupportedDBVersionException.class)
public void unsupportedDBCheck(){
…?
}
@RunWith(CustomTestRunner.class)
public class TestWordDealUtil {
…?
}
package com.ai92.cooljunit;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
…?
/**
* 扚w试 工具?中测试类
* @author Ai92
*/
@RunWith(Suite.class)
@Suite.SuiteClasses({TestWordDealUtil.class})
public class RunAllUtilTestsSuite {
}
package com.ai92.cooljunit;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class TestWordDealUtilWithParam {
private String expected;
private String target;
@Parameters
public static Collection words(){
return Arrays.asList(new Object[][]{
{"employee_info", "employeeInfo"}, //试一般的处理情况
{null, null}, //试 null 时的处理情况
{"", ""}, //试I字W串时的处理情况
{"employee_info", "EmployeeInfo"}, //试当首字母大写时的情况
{"employee_info_a", "employeeInfoA"}, //试当尾字母为大写时的情?
{"employee_a_info", "employeeAInfo"} //试多个相连字母大写时的情况
});
}
/**
* 参数化测试必ȝ构造函?
* @param expected 期望的测试结果,对应参数集中的第一个参?
* @param target 试数据Q对应参数集中的W二个参?
*/
public TestWordDealUtilWithParam(String expected , String target){
this.expected = expected;
this.target = target;
}
/**
* 试?Java 对象名称到数据库名称的{?
*/
@Test public void wordFormat4DB(){
assertEquals(expected, WordDealUtil.wordFormat4DB(target));
}
}
回页?/font>
? Ant Runtime 首选项
<?xml version="1.0"?>
<!-- =============================================
auto unittest task
ai92
========================================== -->
<project name="auto unittest task" default="junit and report" basedir=".">
<property name="output folder" value="bin"/>
<property name="src folder" value="src"/>
<property name="test folder" value="testsrc"/>
<property name="report folder" value="report" />
<!-- - - - - - - - - - - - - - - - - -
target: test report folder init
- - - - - - - - - - - - - - - - - -->
<target name="test init">
<mkdir dir="${report folder}"/>
</target>
<!-- - - - - - - - - - - - - - - - - -
target: compile
- - - - - - - - - - - - - - - - - -->
<target name="compile">
<javac srcdir="${src folder}" destdir="${output folder}" />
<echo>compilation complete!</echo>
</target>
<!-- - - - - - - - - - - - - - - - - -
target: compile test cases
- - - - - - - - - - - - - - - - - -->
<target name="test compile" depends="test init">
<javac srcdir="${test folder}" destdir="${output folder}" />
<echo>test compilation complete!</echo>
</target>
<target name="all compile" depends="compile, test compile">
</target>
<!-- ========================================
target: auto test all test case and output report file
===================================== -->
<target name="junit and report" depends="all compile">
<junit printsummary="on" fork="true" showoutput="true">
<classpath>
<fileset dir="lib" includes="**/*.jar"/>
<pathelement path="${output folder}"/>
</classpath>
<formatter type="xml" />
<batchtest todir="${report folder}">
<fileset dir="${output folder}">
<include name="**/Test*.*" />
</fileset>
</batchtest>
</junit>
<junitreport todir="${report folder}">
<fileset dir="${report folder}">
<include name="TEST-*.xml" />
</fileset>
<report format="frames" todir="${report folder}" />
</junitreport>
</target>
</project>
? junitreport 生成的测试报?/b>
回页?/font>
回页?/font>
描述
名字
大小
下蝲Ҏ
本文CZ代码
coolJUnit.zip
24 KB
HTTP
关于下蝲Ҏ的信?/font>
学习
获得产品和技?/b>
]]>
(2)开始实践,写一个测试接口,起名为LoginTestInfQ?/STRONG>
/*
* Created on 2004-12-17
*
* TODO To change the template for this generated file go to
* Window - Preferences - Java - Code Style - Code Templates
*/
package org.apollo.test.util;
/**
* @author SixSun
*
* TODO To change the template for this generated type comment go to
* Window - Preferences - Java - Code Style - Code Templates
*/
/**
*试用例~号 : 0001
*试用例名称 : HttpUnit 登陆验证试用例
*试目标 : 验证用户登陆是否成功
*试q程 :
*1、输入登陆地址的页面地址Q验证该面是否可被正常讉K?BR> *2、验证被讉K的页面是否是登陆面?BR> *3、输入非法用户名、密码,验证登陆p|?BR> *4、输入合法用户名、密码,验证登陆成功?BR> */
public interface LoginTestInf {
public void testValidPage() throws Exception;
public void testIsLoginPage() throws Exception;
public void testBadLogin() throws Exception;
public void testGoodLogin() throws Exception;
}
(3)实现一个Junit TestCase 同时 implements LoginTestInf 接口Q?/STRONG>
/*
* Created on 2004-12-17
*
* TODO To change the template for this generated file go to
* Window - Preferences - Java - Code Style - Code Templates
*/
package org.apollo.test.util;
import java.net.URL;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.textui.TestRunner;
import com.meterware.httpunit.WebConversation;
import com.meterware.httpunit.WebRequest;
import com.meterware.httpunit.WebResponse;
import com.meterware.httpunit.WebForm;
import com.meterware.httpunit.GetMethodWebRequest;
import org.apollo.test.util.LoginTestInf;
/**
* @author sixsun
*
* TODO To change the template for this generated type comment go to
* Window - Preferences - Java - Code Style - Code Templates
*/
public class LoginTest extends TestCase implements LoginTestInf {
private String username = "suibian";
private String password = "suibian";
private WebConversation browser;
private WebRequest requestIndex;
private WebRequest requestLogin;
private WebResponse responseIndex;
private WebResponse responseLogin;
private String urlSystem = "pȝ首页|址";
private String urlLogin = "登陆界面|址";
/*
* @see TestCase#setUp()
*/
protected void setUp() throws Exception {
browser = new WebConversation();
requestIndex = new GetMethodWebRequest(urlSystem);
responseIndex = browser.getResponse(requestIndex);
requestLogin = new GetMethodWebRequest(urlLogin);
responseLogin = browser.getResponse(requestLogin);
}
//输入登陆地址的页面地址Q验证该面是否可被正常讉K
public void testValidPage() throws Exception{
assertNotNull("zsonline在网l上不存在!",responseIndex);
}
//验证被访问的面是否是登陆页?BR> public void testIsLoginPage() throws Exception{
URL currentUrl = responseLogin.getURL();
String currentUrlStr = currentUrl.getProtocol() + "://" +currentUrl.getHost() + currentUrl.getPath();
assertEquals("登陆面不是zsonline首页!" ,currentUrlStr,urlLogin);
}
//输入非法用户名、密码,验证登陆p|
public void testBadLogin() throws Exception{
WebForm form = responseLogin.getForms()[0];
form.setParameter("username","badname");
form.setParameter("password","badpassword");
requestLogin = form.getRequest();
responseLogin = browser.getResponse(requestLogin);
assertTrue("用户名不存在Q请认用户名输入是否完全正?区分大小?Q?,
responseLogin.getText().indexOf("用户名不存在Q请认用户名输入是否完全正?区分大小?Q?) != -1);
}
//输入合法用户名、密码,验证登陆成功
public void testGoodLogin() throws Exception{
WebForm form = responseLogin.getForms()[0];
form.setParameter("username",username);
form.setParameter("password",password);//此处需要填写真实密?BR> requestLogin = form.getRequest();
responseLogin = browser.getResponse(requestLogin);
assertTrue("转到'zsonline'【suibian】用户首失败!",responseLogin.getText().indexOf("用户试用户_zsonlineQ您好!") != -1);
}
public static TestSuite suite(){
return new TestSuite(LoginTest.class);
}
public static void main(String args[]){
TestRunner.run(suite());
}
}
HttpUnit是一个集成测试工P主要xWeb应用的测试,提供的帮助类让测试者可以通过Javacd服务器进行交互,q且服务器端的响应当作文本或者DOM对象q行处理。HttpUnitq提供了一个模拟Servlet容器Q让你可以不需要发布ServletQ就可以对Servlet的内部代码进行测试。本文中作者将详细的介l如何用HttpUnit提供的类完成集成试?/P>
1 HttpUnit?/B>
HttpUnit是SourceForge下面的一个开源项目,它是ZJUnit的一个测试框Ӟ主要x于测试Web应用Q解决用JUnit框架无法对远EWeb内容q行试的弊端。当前的最新版本是1.5.4。ؓ了让HtpUnit正常q行Q你应该安装JDK1.3.1或者以上版本?/P>
1.1 工作原理
HttpUnit通过模拟览器的行ؓQ处理页面框ӞframesQ?cookies,面跌{QredirectsQ等。通过HttpUnit提供的功能,你可以和服务器端q行信息交互Q将q回的网内容作为普通文本、XML Dom对象或者是作ؓ链接、页面框架、图像、表单、表格等的集合进行处理,然后使用JUnit框架q行试Q还可以导向一个新的页面,然后q行新页面的处理Q这个功能你可以处理一l在一个操作链中的面?/P>
1.2 和其他商业工LҎ
商业工具一般用记录、回攄功能来实现测试,但是q里有个~陷Q就是当面设计被修改以后,q些被记录的行ؓ׃能重用了Q需要重新录制才能l测试?/P>
举个例子Q如果页面上有个元素最先的设计是采用单选框Q这个时候你开始测试,那么q些工具记录的就是你的单w择动作Q但是如果你的设计发生了变化Q比如说我改成了下拉选择Q或者用文本框接受用户输入Q这时候,你以前录制的试q程无效了Q必要重新录制?/P>
而HttpUnit因ؓxҎq些控g的内容,所以不你的外在表现Ş式如何变化,都不影响你已定试的可重用性?/P>
更多的关于httpunit的信息请讉Khttpunit的主?A >http://httpunit.sourceforge.net
2 作者的演示环境
pȝq_QWindows 2000 Server
应用服务器:深圳金蝶的apusic3.0
开发工P eclipse 2.1.2
3 HttpUnit安装、环境配|?/B>
3.1 安装
1. 到HttpUnit的主http://httpunit.sourceforge.net下蝲最新的包文Ӟ当前的最新版本是1.5.4?
2. 下载的Zip包解压羃到c:/httpunit(后面?httpunit_home%引用该目?
3.2 环境配置
作者的演示E序都是在eclipse中开发、执行的Q所以环境配|都是以eclipseZQ如果你使用其他的开发工Ph据这些步骤进行环境配|?/P>
4 如何使用httpunit处理面的内?/B>
WebConversationcLHttpUnit框架中最重要的类Q它用于模拟览器的行ؓ。其他几个重要的cLQ?/P>
WebRequestc,模仿客户hQ通过它可以向服务器发送信息?/P>
WebResponsec,模拟览器获取服务器端的响应信息?/P>
4.1 获取指定面的内?/B>
4.1.1 直接获取面内容
System.out.println("直接获取|页内容Q?); //建立一个WebConversation实例 WebConversation wc = new WebConversation(); //向指定的URL发出hQ获取响? WebResponse wr = wc.getResponse( "http://localhost:6888/HelloWorld.html" ); //用getTextҎ获取相应的全部内? //用System.out.println获取的内容打印在控制台? System.out.println( wr.getText() );
4.1.2 通过GetҎ讉K面q且加入参数
System.out.println("向服务器发送数据,然后获取|页内容Q?); //建立一个WebConversation实例 WebConversation wc = new WebConversation(); //向指定的URL发出h WebRequest req = new GetMethodWebRequest( "http://localhost:6888/HelloWorld.jsp" ); //l请求加上参? req.setParameter("username","姓名"); //获取响应对象 WebResponse resp = wc.getResponse( req ); //用getTextҎ获取相应的全部内? //用System.out.println获取的内容打印在控制台? System.out.println( resp.getText() );
4.1.3 通过PostҎ讉K面q且加入参数
System.out.println("使用Post方式向服务器发送数据,然后获取|页内容Q?); //建立一个WebConversation实例 WebConversation wc = new WebConversation(); //向指定的URL发出h WebRequest req = new PostMethodWebRequest( "http://localhost:6888/HelloWorld.jsp" ); //l请求加上参? req.setParameter("username","姓名"); //获取响应对象 WebResponse resp = wc.getResponse( req ); //用getTextҎ获取相应的全部内? //用System.out.println获取的内容打印在控制台? System.out.println( resp.getText() );
大家x一下上面代码中打了下划U的两处内容Q应该可以看刎ͼ使用Get、PostҎ讉K面的区别就是用的h对象不同?/P>
4.2 处理面中的链接
q里的演C是扑ֈ面中的某一个链接,然后模拟用户的单为,获得它指向文件的内容。比如在我的面HelloWorld.html中有一个链接,它显C的内容是TestLinkQ它指向我另一个页面TestLink.htm. TestLink.htm里面只显CTestLink.html几个字符?/P>
下面是处理代码:
System.out.println("获取面中链接指向页面的内容Q?); //建立一个WebConversation实例 WebConversation wc = new WebConversation(); //获取响应对象 WebResponse resp = wc.getResponse( "http://localhost:6888/HelloWorld.html" ); //获得面链接对象 WebLink link = resp.getLinkWith( "TestLink" ); //模拟用户单击事g link.click(); //获得当前的响应对? WebResponse nextLink = wc.getCurrentPage(); //用getTextҎ获取相应的全部内? //用System.out.println获取的内容打印在控制台? System.out.println( nextLink.getText() );
4.3 处理面中的表格
表格是用来控刉面显C的常规对象Q在HttpUnit中用数l来处理面中的多个表格Q你可以用resp.getTables()Ҏ获取面所有的表格对象。他们依照出现在面中的序保存在一个数l里面?/P>
[注意] Java中数l下标是?开始的Q所以取W一个表格应该是resp.getTables()[0]Q其他以此类推?/P>
下面的例子演C如何从面中取出第一个表格的内容q且他们@环显C出来:
System.out.println("获取面中表格的内容Q?); //建立一个WebConversation实例 WebConversation wc = new WebConversation(); //获取响应对象 WebResponse resp = wc.getResponse( "http://localhost:6888/HelloWorld.html" ); //获得对应的表格对? WebTable webTable = resp.getTables()[0]; //表格对象的内容传递给字符串数l? String[][] datas = webTable.asText(); //循环昄表格内容 int i = 0 ,j = 0; int m = datas[0].length; int n = datas.length; while (i<n){ j=0; while(j<m){ System.out.println("表格中第"+(i+1)+"行第"+ (j+1)+"列的内容是:"+datas[i][j]); ++j; } ++i; }
4.4 处理面中的表单
表单是用来接受用戯入,也可以向用户昄用户已输入信息(如需要用户修Ҏ据时Q通常会显CZ以前输入q的信息Q,在HttpUnit中用数l来处理面中的多个表单Q你可以用resp.getForms()Ҏ获取面所有的表单对象。他们依照出现在面中的序保存在一个数l里面?/P>
[注意] Java中数l下标是?开始的Q所以取W一个表单应该是resp.getForms()[0]Q其他以此类推?/P>
下面的例子演C如何从面中取出第一个表单的内容q且他们@环显C出来:
System.out.println("获取面中表单的内容Q?); //建立一个WebConversation实例 WebConversation wc = new WebConversation(); //获取响应对象 WebResponse resp = wc.getResponse( "http://localhost:6888/HelloWorld.html" ); //获得对应的表单对? WebForm webForm = resp.getForms()[0]; //获得表单中所有控件的名字 String[] pNames = webForm.getParameterNames(); int i = 0; int m = pNames.length; //循环昄表单中所有控件的内容 while(i<m){ System.out.println("W?+(i+1)+"个控件的名字?+pNames[i]+ "Q里面的内容?+webForm.getParameterValue(pNames[i])); ++i; }
5 如何使用httpunitq行试
5.1 寚w面内容进行测?/B>
httpunit中的q部分测试完全采用了JUnit的测试方法,即直接将你期望的l果和页面中的输出内容进行比较。不q这里的试q单多了,只是字符串和字符串的比较?/P>
比如你期望中的页面显C是中有一个表|它是面中的W一个表|而且他的W一行第一列的数据应该是显CusernameQ那么你可以使用下面的代码进行自动化试Q?/P>
System.out.println("获取面中表格的内容q且q行试Q?); //建立一个WebConversation实例 WebConversation wc = new WebConversation(); //获取响应对象 WebResponse resp = wc.getResponse( "http://localhost:6888/TableTest.html" ); //获得对应的表格对? WebTable webTable = resp.getTables()[0]; //表格对象的内容传递给字符串数l? String[][] datas = webTable.asText(); //对表格内容进行测? String expect = "中文"; Assert.assertEquals(expect,datas[0][0]);
5.2 对Servletq行试
除了寚w面内容进行测试外Q有时候(比如开发复杂的Servlets的时候)Q你需要对Servlet本n的代码块q行试Q这时候你可以选择HttpUnitQ它可以提供一个模拟的Servlet容器Q让你的Servlet代码不需要发布到Servlet容器Q如tomcatQ就可以直接试?
5.2.1 原理?/B>
使用httpunit试ServletӞ请创Z个ServletRunner的实例,他负责模拟Servlet容器环境。如果你只是试一个Servlet,你可以直接用registerServletҎ注册q个ServletQ如果需要配|多个ServletQ你可以~写自己的web.xmlQ然后在初始化ServletRunner的时候将它的位置作ؓ参数传给ServletRunner的构造器?/P>
在测试ServletӞ应该记得使用ServletUnitClientcM为客LQ他和前面用q的WebConversation差不多,都承自WebClientQ所以他们的调用方式基本一致。要注意的差别是Q在使用ServletUnitClientӞ他会忽略URL中的L地址信息Q而是直接指向他的ServletRunner实现的模拟环境?/P>
5.2.2 单测?/B>
本实例只是演C如何简单的讉KServletq且获取他的输出信息Q例子中的Servlet在接到用戯求的时候只是返回一串简单的字符ԌHello World!.
1. Servlet的代码如下:
public class MyServlet extends HttpServlet { public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { PrintWriter out = resp.getWriter(); //向浏览器中写一个字W串Hello World! out.println("Hello World!"); out.close(); } }
2. 试的调用代码如下:
//创徏Servlet的运行环? ServletRunner sr = new ServletRunner(); //向环境中注册Servlet sr.registerServlet( "myServlet", MyServlet.class.getName() ); //创徏讉KServlet的客L ServletUnitClient sc = sr.newClient(); //发送请? WebRequest request = new GetMethodWebRequest( "http://localhost/myServlet" ); //获得模拟服务器的信息 WebResponse response = sc.getResponse( request ); //获得的l果打印到控制台? System.out.println(response.getText());
5.2.3 试Servlet的内部行?/B>
对于开发者来_仅仅试h和返回信息是不够的,所以HttpUnit提供的ServletRunner模拟器可以让你对被调用Servlet内部的行行测试。和单测试中不同Q这里用了InvocationContext获得该Servlet的环境,然后你可以通过InvocationContext对象针对request、response{对象或者是该Servlet的内部行为(非服务方法)q行操作?/P>
下面的代码演CZ如何使用HttpUnit模拟Servlet容器Qƈ且通过InvocationContext对象Q测试Servlet内部行ؓ的大部分工作Q比如控制request、session、response{?/P>
//创徏Servlet的运行环? ServletRunner sr = new ServletRunner(); //向环境中注册Servlet sr.registerServlet( "InternalServlet", InternalServlet.class.getName() ); //创徏讉KServlet的客L ServletUnitClient sc = sr.newClient(); //发送请? WebRequest request = new GetMethodWebRequest( "http://localhost/InternalServlet" ); request.setParameter("pwd","pwd"); //获得该请求的上下文环? InvocationContext ic = sc.newInvocation( request ); //调用Servlet的非服务Ҏ InternalServlet is = (InternalServlet)ic.getServlet(); is.myMethod(); //直接通过上下文获得request对象 System.out.println("request中获取的内容Q?+ic.getRequest().getParameter("pwd")); //直接通过上下文获得response对象,q且向客L输出信息 ic.getResponse().getWriter().write("haha"); //直接通过上下文获得session对象Q控制session对象 //lsession赋? ic.getRequest().getSession().setAttribute("username","timeson"); //获取session的? System.out.println("session中的|"+ic.getRequest().getSession().getAttribute("username")); //使用客户端获取返回信息,q且打印出来 WebResponse response = ic.getServletResponse(); System.out.println(response.getText());
[注意]
在测试Servlet的之前,你必通过InvocationContext完成Servlet中的serviceҎ中完成的工作Q因为通过newInvocationҎ获取InvocationContext实例的时候该Ҏq没有被调用?/P>
6 ȝ
本文中,作者详l的演示和介l了如何使用HttpUnit提供的类来进行集成测试,主要实现以下操作Q?/P>
参考资?/B>
以下是一份简单的教程Q向您展C如何用Junit~写和组l测试程序?BR>
一个简单的试用例?/FONT>
您是怎样~写试代码的呢Q?/P>
在调试器中用表辑ּ也许是最单的办法。您可以不用重新~译Q就能改变调试器中的表达式,您甚臛_以在您看到运行的对象实例后再军_如何改变Q您也可以写一些作为测试的表达式将l果打印到标准输出。以上风格的试都有一些局限,因ؓ它们都需要h为的判断来分析程序运行的l果Q而且Q呈现给您的也是一些不友好的输出。您每次只能q行一个调试表辑ּQ如果一个程序有太多的输句将D您很难找到您需要的l果?/P>
JUnit Test不需要h的判断去解释Q而且一ơ可以运行很多的试。如果您需要测试某个东东的时候,您只要这么做卛_Q?/P>
例如Qؓ了测试同一货币单位的两个钱数的和,我们包含了一个真实的DCZq两个钱数的和。如下:
public void testSimpleAdd()
{
Money m12CHF = new Money(12,"CHF");
Money m14CHF = new Money(14,"CHF");
Money expected= new Money(26,"CHF");
Money result = m12CHF.add(m14CHF)
assertTrue(expected.equals(result));
}
如果Q您要写的测试与以前写过的有些类|那就写一个模ѝ如果,您想q行多个试Q那徏立一个组?/P>
模板Q?/P>
当您有两个或多个试需要操作对象的同一或相q部分,该怎么办?
试需要运行在部分内容已经定的对象上Q这些已知的部分被称作测试模ѝ当您在写测试的时候,您通常会发现您构徏试环境Q已知部分)的时间要比您真正比较试l果的时间要ѝ?/P>
从某U程度上_您如果仔l用构造函敎ͼ您写模板的时候也许更Ҏ些。不怎么P许多的保存内Ҏ自共享的模板。通常Q您能够这个模板应用到一些不同的试上。每个测试用例将传递相q的信息或参数给模板Q然后检查不同的l果?/P>
当您写一个通用的模板时Q下面是您所要做的:
例如Qؓ了写一些用到 12瑞士法郎Q?4瑞士法郎Q?8元不同l合的测试用例,那就首先写一个模板:
public class MoneyTest extends TestCase
{
private Money f12CHF;
private Money f14CHF;
private Money f28USD;
protected void setUp()
{
f12CHF = new Money(12,"CHF");
f14CHF = new Money(14,"CHF");
f28USD = new Money(28,"USD");
}
}
一旦您写完了模板,那么Q您可以再写随意多的测试用例了?/P>
试用例
当您拥有了模板后Q您是怎样来写和调用单独的试用例呢?
当没有模板的时候,写测试用例是单的--只需覆写TestCase的匿名子cM的runTestҎ。有模板后,生成TestCase的子cL写设|的代码。然后,为单独的试用例写匿名子cR然而,当写q一些测试以后,您将注意刎ͼ很多的代码行都浪费在语法上了?/P>
JUnit提供了一个简l的Ҏ来利用模板写试Q如下:
1Q在包含模板的类中提供一个public void ҎQ通常U定Q方法名以test开头?/P>
例如Qؓ了测试Moeny 和MoneyBag的和Q如下:
public void testMoneyMoneyBag()
{
//[12 CHF] +[14 CHF] +[28 USD] == {[26 CHF] [28 USD] }
Money bag[] = {f26CHF,f28USD};
MoneyBag expected = new MoneyBag(bag);
assertEquals(expected,f12CHF.add(f28USD.add(f14CHF)));
}
创徏一个MoneyTest实例来运行这个用例的ҎQ如下:
new MoneyTest("testMoneyMoneyBag")
当这个测试运行时Q这个参数名字被用来查找需要运行的Ҏ?/P>
当您有多个测试用例时Q可以将他们l织?套g)suite.
套gQsuiteQ?/B>
您怎样才能一ơ运行多个测试?
只要您有了两个测试,您可能就希望一赯行他们。您当然可以每次只运行一个,但是很快您就会感到厌倦。JUnit提供了一个对象,TestSuiteQ以方便您一ơ完成Q意多的测试一赯行?/P>
例如Q只q行一个测试用例,您可能会执行Q?/P>
TestResult result = (new MoneyTest("testMoneyMoneyBag")).run();
q行两个试用例Q可以先产生一个套?Suite),然后这两个试用例包含其中Q如下:
TestSuite suite = new TestSuite();
suite.addTest(new MoneyTest("testMoneyMoneyBag"));
suite.addTest(new MoneyTest("testSimpleAdd"));
TestResult result = suite.run();
您可以采取另外的一U方式来一ơ运行多个测试用例,那就是让JUnit自己从用例类(TestCase)中提取套?Suite)。您可以通过用例类QTestCaseQ的cd传递给套g(Suite)的构造函数来做到q点?FONT size=6>
TestSuite suite = new TestSuite(MoneyTest.class);
TestResult result = suite.run();
使用手工Ҏ的多数情冉|Q我们希望套件中只包含测试用例的一个子集。其他情况,推荐使用自动提取试套gҎQ它能够避免当您在新d了一个测试用例后Q还需要更改TestSuite(套g)产生代码?FONT size=6>
TestSuites(套g)不仅可以包含试用例Q它q可以包含实现Test接口的Q意对象。例如,您可以在您的代码中生一个套Ӟ同时Q我也生一个,然后我们可以产生一个包含上qC个套件的套g来一赯行?/P>
TestSuite suite = new TestSuite();
suite.addTest(Kent.suite());
suite.addTest(Erich.suite());
TestResult result = suite.run();
TestRunner(试执行?
您怎样q行试Qƈ攉执行后的l果Q?/P>
当您有了一个测试套件的时候,您就惌行它。Juint提供了工h定义q个套gq行q显C测试结果,您需使您的套件能被TestRunner(试q行?讉KQ您可以使用静态方法suite()Qƈ且返回一个suite(套g)来完成这工作?/P>
public static Test suite()
{
TestSuite suite = new TestSuite();
suite.addTest(new MoneyTest("testMoneyEquals"));
suite.addTest(new MoneyTest("testSimpleAdd"));
return suite;
}
或则Q采用自动提取的方式Q?/P>
public static Test suite() {
return new TestSuite(MoneyTest.class);
}
如果QTestCase没有定义suiteҎQ测试执行器自动尝试提取一个suite,q把以test开头的Ҏ装入套g?/P>
Juint提供了图形和文本两种方式的测试执行器Q启动方式分别是Qjava junit.awtui.TestRunner 或则 java junit.swingui.TestRunner.
囑Ş界面的执行方式提供了一个窗口,内容包括Q?/P>
当测试不通过Ӟjuint在底部提供一个失败测试的报表。juint区分p|和错误。失败是预期的,q且使用断言assertions来做查的.错误是没有预计到的,象数l烦引越界。下囑含了一个失败的试?/P>
当您改变代码后,您必重新启动图形界面窗口,q是J琐和耗时的。ؓ了避免这U情况,JUnit 的AWT 和Swing UIs 可以利用junit.runner.LoadingTestCollector Q这个工具在试的每ơ运行时都重新读入您的类。如果您惛_闭这个功能,L“reload classes”属性选项卛_。在帮助中您可以扑ֈ更详l的信息?/P>
有一个批处理来启动Junit.您可以在命o行中键入java junit.textui.TestRunner Q后跟包含suiteҎ的类名。这个方式得C些终端文本输出。另外一U启动的方式是在您的TestCasecMQ包含如下定义的mainҎ?/P>
例如Q启动MoneyTest的测试执行器Q?/P>
public static void main(String args[]) {
junit.textui.TestRunner.run(suite());
}
当您定义了这个mainҎ后,您就可以在命令行中键入:java MoneyTest 来运行您的测试了?/P>
无论是图形方式还是文本方式,都要认在您的classpath上是否包含了junit.jar.
在本文中QTestCase--试用例QSuite--套gQTestRunner--试执行器,————译?/P>
使用最行的开放资源测试框架之一学习单元试基础?/FONT>
使用JUnit可以大量减少Java代码中程序错误的个数QJUnit是一U流行的单元试框架Q用于在发布代码之前对其q行单元试。现在让我们来详l研I如何用诸如JUnit、Ant和Oracle9i JDeveloper{工h~写和运行单元测试?/FONT>
Z么用JUnitQ?/FONT>
多数开发h员都同意在发布代码之前应当对其进行测试,q利用工兯行回归(regressionQ测试。做q项工作的一个简单方法是在所有JavacM以main()Ҏ实施试。例如,假设使用ISO格式Q这意味着有一个以q一格式作ؓ参数的构造器和返回一个格式化的ISO字符串的toString()ҎQ以及一个GMT时区来编写一个Date的子cR?/FONT>清单1 是q个cȝ一个简单实现?/FONT>
不过Q这U测试方法ƈ不需要单元测试限定语QqualifierQ,原因如下Q?/FONT>
JUnit框架是设计用来解决q些问题的。这一框架主要是所有测试实例(UCؓ"TestCase"Q的一个父c,q提供工hq行所~写的测试、生成报告及定义试包(test suiteQ?/FONT>
让我们ؓIsoDatecȝ写一个测试:q个IsoDateTestcȝgQ?
import java.text.ParseException; import junit.framework.TestCase; /** * Test case for <code>IsoDate</code>. */ public class IsoDateTest extends TestCase { public void testIsoDate() throws Exception { IsoDate epoch=new IsoDate( "1970-01-01 00:00:00 GMT"); assertEquals(0,epoch.getTime()); IsoDate eon=new IsoDate( "2001-09-09 01:46:40 GMT"); assertEquals( 1000000000L*1000,eon.getTime()); } public void testToString() throws ParseException { IsoDate epoch=new IsoDate(0); assertEquals("1970-01-01 00:00:00 GMT",epoch.toString()); IsoDate eon=new IsoDate( 1000000000L*1000); assertEquals("2001-09-09 01:46:40 GMT",eon.toString()); } }
本例中要注意的重Ҏ已经~写了一个用于测试的独立c,因此可以对这些文件进行过滤,以避免将q一代码嵌入到将要发布的文档中。另外,本例qؓ你希望在你的代码中测试的每个Ҏ~写了一个专用测试方法,因此你将切地知道需要对哪些Ҏq行试、哪些方法工作正总及哪些方法工作不正常。如果在~写实施文档之前已经~写了该试Q你可以利用它来衡量工作的q展情况?/FONT>
安装q运行JUnit
要运行此CZ试实例Q必首先下载ƈ安装JUnit。JUnit的最新版本可以在JUnit的网?/FONT> www.junit.org免费下蝲。该软g包很(U?00KBQ,但其中包括了源代码和文档。要安装此程序,应首先对该Y件包q行解压~(junitxxx.zipQ。它创Z个目录(junitxxxQ,在此目录下有文档Q在doc目录中)、框架的应用~程接口QAPIQ文档(在javadoc目录中)、运行程序的库文Ӟjunit.jarQ以及示例测试实例(在junit目录中)。截x撰写本文ӞJUnit的最新版本ؓ3.8.1Q我是在此版本上对示例进行测试的?/FONT>
![]() |
要运行此试实例Q将源文ӞIsoDate.java?/FONT>IsoDateTest.javaQ拷贝到Junit的安装目录下Q打开l端Q进入该目录Q然后输入以下命令行Q如果你正在使用UNIXQ:
export CLASSPATH=.:./junit.jar javac *.java 或者,如果你正在WindowsQ输入以下命令行 set CLASSPATH=.;junit.jar javac *.java
q些命o行对CLASSPATHq行讄Q其包含当前目录中的类和junit.jar库,q编译Java源文件?/FONT>
要在l端上运行该试Q输入以下命令行Q?/SPAN>
java junit.textui.TestRunner IsoDateTest
此命令行运行该试Qƈ?/FONT>?1所C的控制C昄试l果?/FONT>
才在此工具可以运行类名被传递到命o行中的单个测试。注意:只有对命令行的最后测试才在考虑之内Q以前的试都被忽略了。(看v来像一个程序错误,是吧Q)
JUnitq提供了利用AWTQ抽象窗口工具包Q或Swingq行试的图形界面。ؓ了利用此囑Ş界面q行试Q在l端上输入以下命令行Q?/SPAN>
java junit.awtui.TestRunner IsoDateTest
或者用Swing界面Q?
java junit.swingui.TestRunner IsoDateTest
此命令行显C?/FONT>?2所C的界面。要选择一个测试ƈ使其q行Q点d有三个点的按钮。这显CCLASSPATHQ还有测试包Q但我们在后面讨论Q中所有测试的列表。要q行试Q点?Run"按钮。测试应当正运行,q在?2所C的界面中显C结果?/FONT>
在此界面中你应当选中复选框"Reload Classes Every Run"Q以便运行器在运行测试类之前对它们进行重新加载。这样就可以方便地编辑、编译ƈq行试Q而不需要每ơ都启动囑Ş界面?/FONT>
在该复选框下面是一个进度条Q在q行较大的测试包Ӟ该进度条非常有用。运行的试、错误和p|的数量都会在q度条下面显C出来。再下面是一个失败列表和一个测试层ơ结构。失败消息显C在底部。通过点击Test HierarchyQ测试层ơ结构)面板Q然后再点击H口右上角的"Run"按钮Q即可运行单个测试方法。请CQ用命令行工具是不可能做到q些的?/SPAN>
注意Q当q行工具来启动测试类Ӟq些cdd在于CLASSPATH中。但是如果测试类存储在jar文g中,那么即ɘq些jar文g存在于CLASSPATH中,JUnit也不能找到这些测试类?/FONT>
![]() |
qƈ不是一U启动测试的方便ҎQ但q运的是QJUnit已经被集成到了其他工P如Ant和Oracle9i JDeveloperQ中Q以帮助你开发测试ƈ使测试能够自动运行?/FONT>
~写Junit试实例
你已l看C试cȝ源代码对IsoDate实施q行了询问。现在让我们来研I这L试文g的实施?/FONT>
试实例由junit.frameword.TestCasel承而来是ؓ了利用JUnit框架的优炏V这个类的名字就是在被测试类的名字上附加"Test"。因Z正在试一个名为IsoDate的类Q所以其试cȝ名字是IsoDateTest。ؓ了访问除U有Ҏ之外的所有方法,q个c通常与被类在同一个包中?BR>
注意Q你必须Z希望试的在cM定义的每个方法都~写一个方法。你要测试构造器或用了ISO日期格式的方法,因此你将需要ؓ以ISO格式的字W串作ؓ参数的构造器和toString()Ҏ~写一个测试方法。其命名方式与测试类的命名方式类|在被试ҎQ或构造器Q前面附?test"?
试Ҏ的主体通过验证assertionQ断aQ对被测Ҏq行询问。例如,在toString()实施的测试方法中Q你希望认该方法已l对旉的设定进行了很好的说明(对于UNIXpȝ来说Q最初问世的旉?970q??日的午夜Q。要实施assertionQ你可以使用Junit框架提供的assertionҎ。这些方法在该框架的junit.framework.AssertcM被实施,q且可以在你的测试中被访问,q是因ؓAssert是TestCase的父cR这些方法可与Java中的关键字assertQ是在J2EE 1.4中新出现的)相比。一些assertionҎ可以查原始类型(如布型、整型等Q之间或对象之间是否相等Q利用equals()Ҏ查两个对象是否相{)。其他assertionҎ查两个对象是否相同、一个对象是否ؓ"I??非空"Q以及一个布|通常׃个表辑ּ生成Q是"?q是"?。在?1中对q些Ҏq行了ȝ?/FONT>
对于那些采用点cd或双_ֺcd参数的assertionQ存在一个第三种ҎQ即采用一个deltag为参数进行比较。另外还要注意,assertEquals()和assertSame()Ҏ一般不会生相同的l果。(两个h相同值的字符串可以不相同Q因为它们是两个h不同内存地址的不同对象。)因此QassertEquals()会验证assertion的有效性,而assertSame()则不会。注意,对于?1 中的每个assertionҎQ你q有一U选择Q就是引入另一个参敎ͼ如果assertionp|Q该参数׃l出一条解释性消息。例如,assertEqualsQint 期望? int 实际|可以与一个诸如assertEqualsQ字W串消息Qint期望|int实际|的消息一起用?
当一个assertionp|Ӟ该assertionҎ会抛Z个AssertFailedError或ComparisonFailure。AssertionFailedError由java.lang.Errorl承而来Q因此你不必在测试方法的throws语句中对其进行声明。而ComparisonFailure由AssertionFailedErrorl承而来Q因此你也不必对其进行声明。因为当一个assertionp|时会在测试方法中抛出一个错误,所以后面的assertion不会l运行。框架捕捉到q些错误q认定该试已经p|后,׃打印Z条说明错误的消息。这个消息由assertion生成Qƈ且被传递到assertionҎQ如果有的话Q?/FONT>
现在下面一行语句添加到testIsoDate()Ҏ的末:
assertEquals("This is a test",1,2);
现在~译q运行测试:
$ javac *.java $ java junit.textui.TestRunner IsoDateTest .F. Time: 0,348 There was 1 failure: 1) testIsoDate(IsoDateTest)junit.framework .AssertionFailedError: This is a test expected:<1> but was:<2> at IsoDateTest.testIsoDate (IsoDateTest.java:29) FAILURES!!! Tests run: 2, Failures: 1, Errors: 0
JUnit为每个已处理的测试打C个点Q显C字?F"来表C失败,q在assertionp|时显CZ条消息。此消息׃发送到assertionҎ的注释和assertion的结果组成(自动生成Q。从q里可以看出assertionҎ的参数顺序对于生成的消息非常重要。第一个参数是期望|而第二个参数则是实际倹{?/FONT>
如果在测试方法中出现了某U错误(例如Q抛Z一个异常)Q该工具׃其昄Z个错误(而不是由assertionp|而生的一?p|"Q。现在对IsoDateTestc进行修改,以将前面增加的一行语句用以下语句代替Q?/SPAN>
throw new Exception("This is a test");
然后~译q运行测试:
$ javac *.java $ java junit.textui.TestRunner IsoDateTest .E. Time: 0,284 There was 1 error: 1) testIsoDate(IsoDateTest)java.lang. Exception: This is a test at IsoDate Test.testIsoDate(IsoDateTest.java:30) FAILURES!!! Tests run: 2, Failures: 0, Errors: 1
该工具将该异常显CZؓ一个错误。因此,一个错误表CZ个错误的试ҎQ而不是表CZ个错误的试实施?/FONT>
Assertc还包括一个fail()ҎQ该版本带有解释性消息)Q该Ҏ通过抛出AssertionFailedError来中断正在运行的试。当你希望一个测试失败而不会调用一个判定方法时Qfail()Ҏ是非常有用的。例如,如果一D代码应当抛Z个异常而未抛出Q那么可以调用fail()Ҏ使该试p|Q方法如下:
public void testIndexOutOfBounds() { try { ArrayList list=new ArrayList(); list.get(0); fail("IndexOutOfBoundsException not thrown"); } catch(IndexOutOfBoundsException e) {} }
JUnit的高U特?/FONT>
在示例测试实例中Q你已经同时q行了所有的试。在现实中,你可能希望运行一个给定的试Ҏ来询问你正编写的实施ҎQ所以你需要定义一l要q行的测试。这是框架的junit.framework.TestSuitecȝ目的Q这个类其实只是一个容器,你可以向其中d一pd试。如果你正在q行toString()实施Qƈ希望q行相应的测试方法,那么你可以通过重写试的suite()Ҏ来通知q行器,Ҏ如下Q?/SPAN>
public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest(new IsoDateTest ("testToString")); return suite; }
在此Ҏ中,你用具体CZ说明了一个TestSuite对象Qƈ向其中添加了试。ؓ了在ҎU定义测试,你可以利用构造器方法名作ؓ参数使测试类实例化。此构造器可按如下Ҏ实施Q?/FONT>
public IsoDateTest(String name) { super(name); }
上面的构造器和方法添加到IsoDateTestc(q需要引入junit.framework.Test和junit.framework.TestSuiteQ,q在l端上输入:
![]() |
?Q选择一个测试方?
$ javac *.java $ java junit.textui.TestRunner IsoDateTest . Time: 0,31 OK (1 test)
注意Q在d到测试包中的试Ҏ中,只运行了一个测试方法,即toString()Ҏ?/FONT>
你也可以利用囑Ş界面Q通过在图3所C的Test Hierarchy面板中选择试Ҏ来运行一个给定的试Ҏ。但是,要注意当整个试包被q行一ơ后Q该面板被填满?/FONT>
当你希望一个测试实例中的所有测试方法添加到一个TestSuite对象中时Q可以用一个专用构造器Q该构造器此试实例的类对象作ؓ参数。例如,你可以用IsoDateTestcd施suite()ҎQ方法如下:
public static Test suite() { return new TestSuite(IsoDateTest.class); }
q有一些情况,你可能希望运行一l由其他试Q如在工E发布之前的所有测试)l成的测试。在q种情况下,你必ȝ写一个实施suite()Ҏ的类Q以建立希望q行的测试包。例如,假定你已l编写了试cAtest和Btest。ؓ了定义那些包含了cATest中的所有测试和在BTest中定义的试包的集合Q可以编写下面的c:
import junit.framework.*; /** * TestSuite that runs all tests. */ public class AllTests { public static Test suite() { TestSuite suite= new TestSuite("All Tests"); suite.addTestSuite(ATest.class); suite.addTest(BTest.suite()); return suite; } }
你完全可以像q行单个试实例那样q行q个试包。注意,如果一个测试在一个套件中d了两ơ,那么q行器将q行它两ơ(试包和q行器都不会查该试是否是唯一的)。ؓ了了解实际的试包的实施Q应当研IJunit本n的测试包。这些类的源代码存在于JUnit安装的junit/test目录下?/FONT>
![]() |
一个main()ҎdC个测试或一个测试包中有时是非常方便的,因此可以在不使用q行器的情况下启动测试。例如,要将AllTests试包作Z个标准的JavaE序启动Q可以将下面的main()Ҏd到类中:
public static void main(String[] args) { junit.textui.TestRunner.run(suite()); }
现在可以通过输入java AllTests来启动这个测试包?
JUnit框架q提供了一U有效利用代码的ҎQ即资源集合到被称为fixture的对象集中。例如,该示例测试实例利用两个叫作epoch和eon的参考日期。将q些日期重新~译到每个方法测试中只是费旉Q而且q可能出现错误)。你可以用fixture重新~写试Q如清单2所C?/FONT>
你定义了两个参考日期,作ؓ试cȝD,q将它们~译C个setUp()Ҏ中。这一Ҏ在每个测试方法之前被调用。与其对应的Ҏ是tearDown()ҎQ它在每个试Ҏq行之后清除所有的资源Q在q个实施中,该方法事实上什么也没做Q因为垃圾收集器为我们完成了q项工作Q。现在编译这个测试实例(其源代码应当攑֜JUnit的安装目录中Qƈq行它:
$ javac *.java $ java junit.textui.TestRunner IsoDateTest2 .setUp() testIsoDate() tearDown() .setUp() testToString() tearDown() Time: 0,373 OK (2 tests)
注意Q在该测试实例中建立了参考日期,因此在Q何测试方法中修改q些日期都不会对其他试产生不利影响。你可以代码放到这两个Ҏ中,以徏立和释放每个试所需要的资源Q如数据库连接)?/FONT>
JUnit发布版还提供了扩展模式(在包junit.extensions中)Q即test decor-atorsQ以提供像重复运行一个给定的试q样的新功能。它q提供了一个TestSuiteQ以方便你在独立的线E中同时q行所有测试,q在所有线E中的测试都完成时停止?/SPAN>
利用Ant使测试自动化 如前面所qͼ试q行器是非常原始的。如果你正在q行Ant来编译你的工E,那么~译文g是运行单元测试的好方法。(关于Ant的介l,请参考我的文章《Ant介》(Starting with AntQ,发表于Oracle杂志2002q?1Q?2月号中)?/FONT> 假设你的源文件在src目录中,所生成的类在tmp目录中,q且junit.jar库位于工E的libdirectory目录中,那么你可以编译Java源文Ӟq?/FONT>清单3中所C的~译文gQ在工程的根目录中)q行单元试?/FONT> q个~译文g的核心是q行单元试的测试目标。运行这些测试是q个目标junit的唯一d。ؓ了运行这一可选Q务,必须首先junit.jar库放到Ant安装目录下的lib目录中,然后下蝲q安装同一目录中的Ant可选Q务库?/FONT>清单3中的CZ嵌套了一个classpathc,它包括JUnit库和工程的类Q示例中q嵌套了一个batchtest元素Q它利用一个选择适当源文件的fileset元素定义了将要运行的试。这个Q务还包括haltonfilure和haltonerror属性,它们告诉Ant在遇C个失败或错误时是否应当停止。如果将它们的D|ؓ"?Q那么Ant在遇到第一个失败或错误时将会停止,~译会p|Q显Ӟq表C在q行试q程中存在有问题Q。另一斚wQ如果将它们的D|ؓ"?Q其l果׃是非常明了Q即使测试失败,~译也会成功Q,但所有测试仍运行。printsummary属性指CAnt是否昄q行试的输出。数值withOutAndErr可以在开发测试时方便地告诉Ant昄标准输出和错误输出。数值off表示不显CZQ何内容,而on只显C测试报告(没有试cȝ输出Q。junitdh很多属性,详细内容请参考Ant的文档?/FONT> Z试q一~译文gQ你需要徏立名字ؓsrc、tmp和lib的目录。将junit.jar库放到lib目录中,q将前面看到的示例Java源文件放到src目录中。打开l端Q进入该工程的根目录Qƈ输入antQ其l果为: Antq可以生成非常有用的HTML格式的测试报告。ؓ了生成这L报告Q将前面的测试目标用以下目标代替Q?/FONT> q一目标与前面的目标相同Q只是该目标在batchtext元素中增加了一个新的属?-todirQ它告诉Ant在tmp目录中生成可扩展的标记语aQXMLQ报告。该目标q增加了一个新的junitreport元素Q以便由XML文g生成一个HTML报告。这一元素要求在安装Ant的lib目录中安装Xalan库(详细内容见Ant文档的junitreport部分Q?/FONT>ant.apache.org/manual/install.htmlQ。这一元素q定义了使用todir属性生成的文g的目标目录。通过嵌套一个fileset元素来定义ؓ生成q一报告而需要处理的XML文g。期望的输出格式利用嵌套的报告元素来实现。该对象生成一个诸?/FONT>?所C的报告? q类报告在单元试自动q行时特别有用(比如在夜间编译期_。在q些情况下,错误或失败不会中断测试,因此你必d前面提到的junitd的haltonfailure和haltonerror属性设|ؓ"?。这一报告对于衡量实施q程也非常有用(比如当你必须重写已有代码Ӟ或者在实施之前已经~写了测试的情况下)?/FONT> Ant对启动JUnit囑Şq行器也非常有用。下面的对象会启动Swing试q行器: 你应当在l端中运行这一对象Qƈ且在另一个终端或你喜Ƣ的IDE中用Ant对其q行~译。这U方式你不必在每次惌试代码旉启动囑Şq行器? 在Oracle9i Jdeveloper中的JUnit集成 Oracle9i JDeveloperq没有基于网l集成JUnitQ但是下载ƈ安装q一插g只需要几分钟的时间。ؓ了完成此q程Q选择JDeveloper?Help"菜单下的"Check for Updates"V这样将会打开IDE更新向导Q以q接到Oracle技术网站,下蝲该插件ƈ安装它。当安装该插件后Q需要关闭ƈ重启Oracle9i JDeveloper。注意,向导q会下蝲相关的文档?/FONT> 通过为每个Q务提供向|q个插g极大地提高了开发h员编写测试实例、测试包和fixture{的工作效率。要调用q些向导Q点?File"菜单下的"New",然后选择"General/Unit Tests"c,q从右侧的窗体中选择合适的向导。你也可以从界面上启动测试套件?/FONT> 当准备好寚w目进行代码测试后Q应当首先用专用向导来~写fixtureQ然后测试实例向导可以利用它们集成到试实例中。另外,q有一些用来生成自定义试fixture的向g及生成商务组件和数据库连接测试fixture的向对{这后两U向导生成专用代码,以用setUp()和tearDown()Ҏ讄和发布商务组件或数据库连接?/FONT> 当完成fixture后,下一步应当用合适的向导来生成测试实例,q些向导可以让你选择要测试的cdҎ。你q可以选择在这个测试中使用的fixture。这生成一个用测试方法的M完成的代码框架。最后应当生成套件来q行你的试。这个专用向D你选择要包括在套g中的试QƈZ生成整个cR要启动一个测试套Ӟ点击览器中的文Ӟq择Run。这会启动囑Ş界面q运行套件的试?/FONT> ?Help"菜单中选择"Help Topics"Q你会在JDeveloper文档中找到关于如何用这些向导的详细教程。这会打开帮助pȝH口。点?Unit Testing with JUnit",然后选择合适的教程?/FONT> JUnit和JDeveloper之间的这U集成你能够只~写单元试中你感兴的那部分的代码Q而让工具Z~写重复的代码?/FONT>
讉KJUnit|站 下蝲 Oracle9i应用服务?/SPAN> 学习Oracle9i JDeveloper扩展 阅读Oracle9i JDeveloper文档 JUnit最佛_?/STRONG> 下面是一些在使用JUnit时应当注意的技巧:
值得p的时?/STRONG> 到现在,你应当已l清楚地知道使用JUnit框架和合适的工具实施单元试是多么快速而简单。关于单元测试的下一个目标是使你的CTO怿你在实施试时所必须p的时间是Z以后节省更多的时间。但是,当你考虑在检查老代码、修正错误和发布一个调试过的版本上所p的时_它可能花Ҏ个一天)Ӟ在开发过E的早期阶段捕获的代码错误毫无疑问是一很好的投资。这里ƈ没有考虑当错误代码不再位于块的顶部时开发h员必遵循的"black magic"步骤Q这些步骤包括:标记代码Q制作一个分支、修正代码错误、进行发布,以及代码修正合q到块中。所有这些步骤都非常耗时Qƈ且容易生错误?/FONT> 要开始用单元测试和JUnitQ请讉KJUnit|站Q?www.junit.org。你找到大量有用的文档Q包括用JUnit实施试的详l说明书Q、一个与JUnit集成的IDE列表Q以及关于JUnit扩展工具的详l内宏V?/FONT> Michel Casabianca ( casa@sweetohm.net)是In-FusioQ一家ؓUd用户提供游戏服务的法国公司)的一名Y件工E师Q同时也是XML Pocket ReferenceQO'Reilly出版Q?001q_一书的合著者?/FONT>
$ ant
Buildfile: build.xml
clean:
[delete] Deleting directory
/Users/casa/doc/oracle
/junit/prj/tmp
[mkdir] Created dir: /Users/casa
/doc/oracle/junit/prj/tmp
bin:
[javac] Compiling 4 source files
to /Users/casa/doc/oracle
/junit/prj/tmp
test:
[junit] Running IsoDateTest
[junit] Tests run: 1, Failures:
0, Errors: 0, Time elapsed:
0,005 sec
[junit] Running IsoDateTest2
[junit] Tests run: 2, Failures: 0,
Errors: 0, Time elapsed: 0,031 sec
[junit] Output:
[junit] setUp()
[junit] testIsoDate()
[junit] tearDown()
[junit] setUp()
[junit] testToString()
[junit] tearDown()
all:
BUILD SUCCESSFUL
Total time: 8 seconds
<target name="test" depends="bin"
description="Run JUnit tests">
<junit haltonfailure="false"
printsummary="withOutAndErr">
<classpath refid="cp"/>
<batchtest todir="${tmp}">
<fileset dir="${src}"
includes="**/*Test*.java"/>
</batchtest>
<formatter type="xml"/>
</junit>
<junitreport todir="${tmp}">
<fileset dir="${tmp}"
includes="TEST-*.xml"/>
<report format="frames"
todir="${tmp}"/>
</junitreport>
</target>
<target name="testui" depends="bin"
description="Run graphical JUnit">
<java classname="junit.swingui.TestRunner"
classpathref="cp"
fork="true"/>
</target>
www.junit.org
Oracle9i Jdeveloper
otn.oracle.com/software/products/jdev/
otn.oracle.com/software/products/ias/
otn.oracle.com/products/jdev/htdocs/partners/addins
otn.oracle.com/docs/products/jdev/
assertEquals(期望原型,实际原型)
查两个原型是否相{?/FONT>
assertEquals(期望对象,实际对象)
利用对象的equals()Ҏ查两个对象是否相{?/FONT>
assertSame(期望对象Q实际对?
查具有相同内存地址的两个对象是否相{?/FONT>
assertNotSame(期望对象,实际对象)
查具有不同内存地址的两个对象是否不相等
assertNull(对象 对象)
查一个对象是否ؓI?/FONT>
assertNotNull(对象 对象)
查一个对象是否ؓ非空
assertTrue(布尔条g)
查条件是否ؓ?/FONT>
assertFalse(布尔条g)
查条件是否ؓ?/FONT>