摘要
本文展示如何使用JUnit测试框架撰写及执行简单的测试案例及测试系列。
原作者:Mike Clark Clarkware Consulting, Inc. October 7, 2000
翻译:Areca Chen 2002/1/23
简介
本文的目的是展示一个撰写及执行JUnit测试案例及测试系列(suites)(或测试包)简单
快速的方法。我们一开始先探讨使用JUnit的主要优点然後撰写一些测试范例以展示其简
单性及效力。
本文包含下列各节:
简介
为什么使用JUnit?
JUnit的设计
第一步:撰写一个测试案例
第二步:撰写一个测试系列
第三步:执行测试
第四步:组织测试
测试惯例
训练及顾问指导
资源
在你开始之前,请确认你已下载并安装下列的软体:
JUnit (3.1 or later)
为什么使用JUnit?
在我们开始之前,我们要问为什么我们要使用JUnit?单元测试的主题呈现在我们脑中的
往往是长夜的思考试著去符合专案测试案例的配额。不管如何,不像传统单元测试的残
酷特性,使用JUnit实际上在你提升程式码的品质时JUnit测试仍允许你更快速的撰写程
式。只要你开始使用JUnit你将开始注意到介於程式码及测试之间有一个强大的结合力,
最终你的开发风格是只有当一个失败的测试时才写新的程式码(only writing new code
when a test is failing)。
以下列出使用JUnit的理由:
在你提升程式码的品质时JUnit测试仍允许你更快速的撰写程式
是的,我知道,那听起来似乎不是很直觉,但那是事实。当你使用JUnit撰写测试,你将
花更少的时间除虫,同时对你程式码的改变更 俱有信心。这个信心让你更积极重整程式
码并增加新的功能。没有测试,对於重整及增加新功能你会变得没有信心;因为你不知
道有甚么东西会破坏产出的结果。采用一个综合的测试系列,你可以在改变程式码之後
快速的执行多个测试并对於你的变动并未破坏任何东西感到有信心。在执行测试时如果
发现臭虫,原始码仍然清楚的在你脑中,因此很容易找到臭虫。在JUnit中撰写的测试帮
助你以一种极 大(extreme)的步伐撰写程式及快速的找出缺点。
JUnit非常简单
撰写测试应该很简单--这是重点!如果撰写测试太复杂或太耗时间,便无法要求程式设
计师撰写测试。使用JUnit你可以快速的撰写测试并检测你的程式码并逐 步随著程式码
的成长增加测试。只要你写了一些测试,你想要快速并频繁的执行测试而不至於中断建
立设计及开发程序。使用JUnit执行测试就像编译你的程式码那么容易。事实上,你应该
执行编译时也执行测试。编译是 检测程式码的语法而测试是检查程式码的完整性(inte
grity)。
JUnit测试检验其结果并提供立即的回馈。
如果你是以人工比对测试的期望与实际结果那么测试是很不好玩的,而且让你的速度慢
下来。JUnit测试可以自动执行并且检查他们自己的结果。当你执行测试,你获得简单且
立即的回馈; 比如测试是通过或失败。而不再需要人工检查测试结果的报告。
JUnit测试可以合成一个测试系列的层级架构。
JUnit可以把测试组织成测试系列;这个测试系列可以包含其他的测试或测试系列。JUn
it测试的合成行为允许你组合多个测试并自动的回归(regression)从头到尾测试整个测
试系列。你也可以执行测试系列层级架构中任何一层的测试。
撰写JUnit测试所费不多。
使用Junit测试框架,你可以很便宜的撰写测试并享受由测试框架所提供的信心。撰写一
个测试就像写一个方法一样简单;测试是检验要测试的程式码并定义期望的结果。这个
测试框架提供自动执行测 试的背景;这个背景并成为其他测试集合的一部份。在测试少
量的投资将持续让你从时间及品质中获得回收。
JUnit测试提升软体的稳定性。
你写的测试愈少;你的程式码变的愈不稳定。测试使得软体稳定并逐步累积信心;因为
任何变动不会造成涟漪效应而漫及整个软体。测试可以形成软体的完整结构的胶结。
JUnit测试是开发者测试。
JUnit测试是高度区域性(localized)测试;用以改善开发者的生产力及程式码品质。不
像功能测试(function test)视系统为一个黑箱以确认软体整体的工作性为主,单元测试
是由内而外测试系统基础的建构区块。开发者撰写并拥有JUnit测试。每当一个开发反覆
(iteration)完成,这个测试便包裹成为交付软体的一部份 提供一种沟通的方式,「这
是我交付的软体并且是通过测试的。」
JUnit测试是以Java写成的。
使用Java测试Java软体形成一个介於测试及程式码间的无缝(seamless)边界。在测试的
控制下测试变成整个软体的扩充同时程式码可以被重整。Java编译器的单元测试静态语
法检查可已帮助测试程序并且确认遵守软体介面的约定。
JUnit是免费的(JUnit is free.)
JUnit的设计
JUnit是以两个关键设计样式来设计的:指令样式(Command pattern)及合成样式(Compo
site pattern)。
TestCase是一个指令物件。任何包含测试方法的类别都是TestCase的子类别。TestCase
可以定义任意数量的testXXX()方法。当你要检查期望与实际的测试结果,你启动asser
t()方法的各种类型(variaton)。
TestCase子类别包含多个testXXX()方法;可以使用setUp()及tesrDown()方法初始化及
释放测试下的任何一般的物件,这个子类别形同测试的基础设备(fixture)。每一个测试
在其本身基础设备的背景下执行,在每一个测试方法之前呼叫setUp()及之後呼叫tearD
own()以确保没有副作用影响测试的执行。
TestCase实例物件可以合成为TestSuite层级架构;在这个TestSuite层级架构中可以自
动启动定义在TestCase实例物件中的所有testXXX()方法。一个TestSuite是其他多个测
试的一个合成物件(composite),其中包括TestCase实例物件及其他的TestSuite实例物
件。这个由TestSuite代表的合成物件行为允许你组合测试的测试系列的测试系列到任意
深度,并且自动一致性(uniformly)的执行所有测试以产出个别的通过或失败的状态。
第一步:撰写一个测试案例
首先,我们将撰写一个测试案例以便检测单一软体元件。我们将聚焦於撰写测试;这个
测试检验这个元件的行为;而这个物件的行为是有著最高的破损的可能性,因此可以从
测试的投资获得最大的回收。
撰写测试案例请依据下列的步骤:
定义一个TestCase的子类别。
覆写setUp()方法以初始化测试中的一个或多个物件。
覆写tearDown()方法以释放测试中的一个或多个物件。
定义一个或多个公开的testXXX()方法;这些方法检验这些测试中的物件并且评估期望的
结果。
定义一个静态的suite()工厂方法;这个工厂方法构建一个TestSuite其中包含TestCase
的所有testXXX()方法。
随意的定义一个main()方法以批次的方式执行TestCase。
下列是一个测试案例的范例:
(这个范例完整的程式码可以在资源一节中找的到。)
测试案例的范例(Example Test Case)
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
public class ShoppingCartTest extends TestCase {
private ShoppingCart _bookCart;
private Product _defaultBook;
/**
*以特定名称建构一个ShoppingCartTest。
*
*建构函数是以测试案例的名称当作参数
*/
public ShoppingCartTest(String name) {
super(name);
}
/**
* 设定测试设备
*在测试案例方法之前呼叫
* /
protected void setUp() {
_bookCart = new ShoppingCart();
_defaultBook = new Product("Extreme Programming", 23.95);
_bookCart.addItem(_defaultBook);
}
/**
*释放测试设备
*
*在测试案例方法之後呼叫
*
*/
protected void tearDown() {
_bookCart = null;
}
/**
*测试在cart中增加一个产品
*
*/
public void testProductAdd() {
Product newBook = new Product("Refactoring", 53.95);
_bookCart.addItem(newBook);
double expectedBalance = _defaultBook.getPrice() + newBook.getPrice(
);
assertEquals(expectedBalance, _bookCart.getBalance(), 0.0);
assertEquals(2, _bookCart.getItemCount());
}
/**
*测试清空cart
*
*/
public void testEmpty() {
_bookCart.empty();
assertTrue(_bookCart.isEmpty());
}
/**
*测试从cart中移除产品
*
*如果此产品不在cart中丢出一个ProductNotFoundException的例外
*
*/
public void testProductRemove() throws ProductNotFoundException {
_bookCart.removeItem(_defaultBook);
assertEquals(0, _bookCart.getItemCount());
assertEquals(0.0, _bookCart.getBalance(), 0.0);
}
/**
*测试从cart中移除一个未知的产品
*
*如果ProductNotFoundException例外产生表示测试成功
*
*/
public void testProductNotFound() {
try {
Product book = new Product("Ender's Game", 4.95);
_bookCart.removeItem(book);
fail("Should raise a ProductNotFoundException");
} catch(ProductNotFoundException success) {
// 测试成功
}
}
/**
*组合并传回一个这个测试案例所有测试方法的测试系列
*传回一个非空值(non-null)的测试系列
*/
public static Test suite() {
//这里使用的想法是加入所有的testXXX()方法到测试系列中。
//
TestSuite suite = new TestSuite(ShoppingCartTest.class);
//下面是另一种作法,但增加愈多的测试案例方法愈有可能发生错误
//
// TestSuite suite = new TestSuite();
// suite.addTest(new ShoppingCartTest("testEmpty"));
// suite.addTest(new ShoppingCartTest("testProductAdd"));
// suite.addTest(new ShoppingCartTest("testProductRemove"));
// suite.addTest(new ShoppingCartTest("testProductNotFound"));
//
return suite;
}
/**
*执行此测试案例(Runs the test case)
*/
public static void main(String args[]) {
junit.textui.TestRunner.run(suite());
}
}
第二步:撰写一个测试系列
其次,我们将撰写一个测试系列其中包含许多测试案例。此测试系列将允许我们从头到
尾执行其所有的测试案例。
撰写测试系列请依循下列的步骤:
定义一个TestCase的子类别。
定义一个静态的suite()工厂方法;这个方法构建一个TestSuite以包含所以的测试。
随意定义一个main()方法以批次方式执行这个TestSuite。
下列是测试系列的范例:
测试系列范例(Example Test Suite)
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
public class EcommerceTestSuite extends TestCase {
/**
* 以特定名称建构一个EcommerceTestSuite
*
*建构函数是以测试案例的名称当作参数
*/
public EcommerceTestSuite(String name) {
super(name);
}
/**
*组合并传回一个测试系列包含所有已知的测试。
*新的测试应该在此加入
*传回一个非空值(non-null)的测试系列
*/
public static Test suite() {
TestSuite suite = new TestSuite();
//我们在前面构建的ShoppingCartTest
//
suite.addTest(ShoppingCartTest.suite());
//另一个测试系列的范例,在测试系列中加入其他的测试系列
//
suite.addTest(CreditCardTestSuite.suite());
return suite;
}
/**
*执行此测试系列
*/
public static void main(String args[]) {
junit.textui.TestRunner.run(suite());
}
}
第三步:执行测试
现在我们撰写了一个测试系列其中包含一堆测试案例及其他的测试系列,我们可以执行
这个测试系列或者其中任何个别的测试案例。执行TestSuite将自动执行所有的TestCas
e及TestSuite实例物件。执行一个TestCase将自动启动其所有公开的testXXX()方法。
JUnit提供文字及图形使用者界面。两种使用者介面都可以指出多少个测试被执行、任何
错误或失败、及一个简单的完成状态。简化使用者介面是快速执行测试的关键。你应该
简单了解就能够执行你的测试并知道测试的状态,就像你在编译上所做的一样。
文字使用者介面(junit.textui.TestRunner)如果通过所有测试则显示『OK』而如果失败
则显示失败讯息。
图形使用者界面(junit.swingui.TestRunner)显示浮动视窗;如果所有测试皆通过则其
中有一个进度杆显示为绿色,否则进度杆显示为红色。
一般而言,TestSuite及TestCase类别应定义一个main()方法;main()利用适当的使用者
介面。我们写的测试到目前为止皆定义一个main()方法来使用文字使用者介面。
由main()定义的使用文字使用者介面执行我们的测试案例时,使用:
java ShoppingCartTest
另一种方式,使用文字使用者介面执行测试,使用:
java junit.textui.TestRunner ShoppingCartTest
或这使用图形使用者介面时,使用:
java junit.swingui.TestRunner ShoppingCartTest
EcommerceTestSuite可以以类似的方法执行。
第四步:组织测试
最後一步是决定测试在我们的开发环境中应存在於何处。
这里有一些建议如何组织我们的测试:
把测试案例建立在与我们要测试的程式码相同的包裹(package)中。例如:com.mydotco
m.ecommerce包裹包含所有的应用程式阶 级的类别及这些元件的测试案例。
在你的原始码资料夹中避免结合应用程式与测试程式码,建立一个镜射(mirrored)的资
料夹结构对应於此包裹结构;并在镜射资料夹中存放你的测试码。
为你的应用程式中的Java包裹定义一个TestSuite类别;在这个TestSuite类别中包含所
有测试这个包裹内之程式的测试。
定义类似的TestSuite类别;此TestSuite类别在此应用程式中的其他包裹(及子包裹)
中构建高阶及低阶测试系列。
确认你的建构程序包含所有测试的编辑物(compilation)。这样做有助於确认你的测试可
以保持与最後的程式码同步以维持测试是最新的。
经由在每一个Java包裹中建立一个TestSuite,在各种包裹层次中,你可以在每一个抽象
层中执行一个TestSuite。例如,你可以定义一个com.mydotcom.AllTests执行系统中所
有的测试,及定义一个com.mydotcom.ecommerce.EcommerceTestSuite只有执行电子交易
元件的测试。
测试层级架构可以扩充到任意的深度。其深度与你开发系统的抽象层次有关,你可以执
行一个相称的测试。只要选择你的系统层次并测试它即可。
下面的范例是测是的层级架构:
测试层级架构范例(Example Testing Hierarchy)
AllTests (Top-level Test Suite)
SmokeTestSuite (Structural Integrity Tests)
EcommerceTestSuite
ShoppingCartTestCase
CreditCardTestSuite
AuthorizationTestCase
CaptureTestCase
VoidTestCase
UtilityTestSuite
MoneyTestCase
DatabaseTestSuite
ConnectionTestCase
TransactionTestCase
LoadTestSuite (Performance and Scalability Tests)
DatabaseTestSuite
ConnectionPoolTestCase
ThreadPoolTestCase
测试惯例
当你测试时请谨记下面的事项:
软体运作良好的事物都是经过测试检验的。
测试一点点,程式码写一点点,测试一点点,程式码写一点点......
请确认所有测试都能100%通过。
每天(夜)至少执行系统中所有的测试一次。
要测试的程式码是最可能错误的区域。
撰写最可能回收测试投资的测试。
如果你使用System.out.println()除虫,写一个测试自动检查其结果。
如果发现臭虫,写一个测试揭露这个臭虫。
如果下次有人要求你帮他除虫,帮他写一个测试。
撰写程式码之前先写单元测试;而且只有当一个测试失败才写新的程式码。
训练及顾问指导
想要学习更多有关测试吗?身为Clarkware Consulting的首席顾问,我提供线上JUnit训
练及顾问指导服务帮助你的团队使用JUnit提升生产力及有效的应用JUnit测试你的专案
。开发服务有可以帮你的专案成功。这些服务可以量身订制以符合你特殊专案的需求。
请以email 或电话720.851.2014与我联络。
资源
Source Code -ShoppingCartTest范例完整的原始码。
JUnit - 官方网站
JUnit Cookbook -手册
JUnit FAQ
"JUnit Test Infected: Programmers Love Writing Tests" by Gamma, E. and Beck,
K.
"JUnit A Cook's Tour" by Gamma, E. and Beck, K.
JUnitPerf -收集JUnit测试修饰者(decorators)用以衡量包含於现有JUnit测试的功能其
执行效率及可达成性(scalability) (A collection of JUnit test decorators used
to measure the performance and scalability of functionality contained within
existing JUnit tests.)
JDepend - 一个Java包裹相关的分析器包含JUnit测试案例的范例。(A Java package
dependency analyzer with example JUnit test cases)
Java Tools for Extreme Programming: Mastering Open Source Tools Including An
t, JUnit, and Cactus, by Richard Hightower, Nicholas Lesiecki (John Wiley &
Sons, 2001)
Refactoring: Improving The Design Of Existing Code, by Fowler, M. (Addison-W
esley, 1999)
Extreme Programming Explained, by Beck, K. (Addison-Wesley, 2000)
----------------------------------------------------------------------------
----
作者简介(About the author)
Mike Clark是Clarkware Consulting, Inc.的首席顾问,提供客户的软体架构、设计、
开发、及执行效率谘询。Mike有十年左右使用各种技术开发及交付软体的经验及五年实
际经验的Java专家;在Java中的经验强调使用J2EE技术於伺服器端的开发。
----------------------------------------------------------------------------