JUnit的框架与应用专题讨论
< span> 开源技术专题群> 异想者
杨树 整理 foxcrane 发布
讲座提纲:
0.背景介绍
1.Junit的使用方法 (实例)
2.Junit的的主要类与方法 (理论)
3.Jmock对Junit的扩展与使用方法 (实例、理论)
4.strutsJunit对Struts的测试示例 (实例)
5.现有测试工具的一览性说明和大概了解 (理论)
6.PMD对静态代码的测试 (实例)
7.测试要注意的问题 (理论)
0. 背景介绍
长期以来,我所接触的软件开发人员很少有人能在开发的过程中进行测试工作。大部分的项目都是在最终验收的时候编写测试文档。有些项目甚至没有测试文档。现在情况有了改变。我们一直提倡UML、RUP、软件工程、CMM,目的只有一个,提高软件编写的质量。所以我们这些凡人--在同一时间只能将注意力集中到若干点(据科学统计,一般的人只能同时考虑最多7个左右的问题,高手可以达到12个左右),而不能既纵览全局又了解细节--只能期望于其他的方式来保证我们所编写的软件质量。测试就是一种很好的保证软件质量的方式。
第一: 测试的目的很简单也极具吸引力,就是写出高质量的软件
第二: 测试是一个持续的过程。也就是说测试贯穿与开发的整个过程中,单元测试尤其适合于迭代增量式(iterative and incremental)的开发过程
第三: 在你不知道如何测试代码之前,就不应该编写程序。而一旦你完成了程序,测试代码也应该完成。除非测试成功,你不能认为你编写出了可以工作的程序。
在XP 中推崇的 Test First Design 就是基于以上的技术。如果你要写一段代码,则需要:
先用 junit 写测试,然后再写代码
写完代码,运行测试,测试失败
修改代码,运行测试,直到测试成功
Java 下的 team 开发,一般采用 cvs(版本控制) + ant(项目管理) + junit(集成测试) 的模式:
每天早上上班,每个开发人员从 cvs server 获取一个整个项目的工作拷贝。
拿到自己的任务,先用 junit 写今天的任务的测试代码。
然后写今天任务的代码,运行测试,直到测试通过,任务完成
在下班前一两个小时,各个开发人员把任务提交到 cvs server
然后由主管对整个项目运行自动测试,哪个测试出错,就找相关人员修改,直到所有测试通过。下班。。。
由此我们可以看到junit面对的需求了。
1.Junit的使用方法 [参考:怎样使用Junit Framework进行单元测试的编写]
Junit是一个单元测试框架,框架是它的重点 。大家先看段代码:
package hibernateTest;
import junit.framework.Test; import junit.framework.TestSuite;
public class TestAll {
// 定义一个suite,对于junit的作用可以视为类似于java应用程序的main。 public static Test suite {
//定义两个TestSuite TestSuite suite = new TestSuite("root hibernate tests"); TestSuite childSuite = new TestSuite("child hibernate tests");
//组成一个测试目录树 suite.addTest(childSuite); childSuite.addTestSuite( TestSample.class); childSuite.addTestSuite( TestSample.class);
System.out.println("测试数据数量:"+suite.countTestCases ); return suite; } } |
这是我用来测试hibernate的一个测试用例管理的主程序。别急,还没到测试代码 J,我们先说说测试框架,它之所以成为一个测试框架而不是一个测试工具,就在于此。它的结果如下图所示:
我们可以看到图中的一个测试树结构。这段代码有以下几个点可以说一下:
定义一个suite,这个方法的声明 对于junit的作用可以视为类似于java应用程序的main。
public static Test suite {……} |
而这个目录树的目录就是下面的代码所示:
//定义两个TestSuite TestSuite suite = new TestSuite("root hibernate tests"); TestSuite childSuite = new TestSuite("child hibernate tests"); |
而目录的组织和结点如下面的代码所示:
suite.addTest(childSuite); childSuite.addTestSuite( TestSample.class); childSuite.addTestSuite( TestSample.class); |
刚才说的是测试目录树的管理,现在准备说的是结点的书写, 也就是测试代码了:
package hibernateTest;
import junit.framework.TestCase; import junit.framework.Test;
public class TestSample extends TestCase { public void testMethod1 { try { boolean b = true; assertTrue("错误则提示该信息",b); } catch (Exception e) { System.out.println("错误则提示该信息"); } } } |
注意以下代码:
public class TestSample extends TestCase |
extends TestCase是关键部分。这样,它就成为了测试函数。
再看下一个关键部分代码:
public void testMethod1 {……} |
testXXX也就是一个一个的测试方法。
这时请大家回顾一下,在一开始贴出的第二个图,也就是测试目录,查看一下,这些代码中的关键部分对树的目录结构的影响。
2.Junit的的主要类与方法
首先在测试框架中,看看这个测试是如何完成的一个流程:
a) TestCase.run 调用TestResult.run
b) TestResult.run 调用TestResult .StartTest
c) TestResult.run 创建一个Anonymous 类,实现接口Portectable
d) 在Portectable. protect 方法中调用TestCase .runBare
e) 通过运行Portectable.runBare 调用runBare,通过Exception 捕获增加错误
解释如下:
首先,一个测试启动,但不会就直接调用测试代码,如果这样,那就不叫测试框架了J。而TestResult就是一个记录结果的类 ,而如果它直接调用测试代码,也会出问题。问题就是,在多个测试同时进行时,一个测试出错后,怎么办? [目前说到b]
接下来,就是Portectable出场了: [目前说到c]
public abstract void protect() throws Throwable; |
通过这种定义可以保证运行的时候如果出现任何Error 和Exception,都将被抛出而不会导致程序不能继续运行。 这也是c完成的工作。在这些准备工作做完之后,就可以调用测试代码。
在这些准备工作完成后,d中的TestCase .runBare就是如实的调用测试代码了。
最后通过Exception 捕获增加错误记录到TestResult中,就可以以文本或图形方式输出了。
在这个过程中还有一些隐含被调用的类和方法如下:
a) 方法throws 的是所有Error 和Exception 的祖先,通过这种定义可以保证运行的时候如果出现任何Error 和Exception,都将被抛出而不会导致程序不能继续运行。
b) TestResult 用于运行并收集测试结果(通过Exception 捕获)。
c) TestListener 接口的用途和它名称一样,用于监听。主要用于运行时刻监听,BaseRunner(所有运行类,如TestRunner)实现了这一接口。由于运行是通过TestResult 来实现,只要调用 TestResult.addListener 就可以增加监听。
d) Assert断言就是是否达到你需要的结果 junit里有fail和exception两种失败 ,如果是被测试的程序中抛出异常,也认为测试失败。
3.Jmock对Junit的扩展与使用方法 [参考:使用jMock辅助单元测试]
Jmock是对Junit的扩展,是以junit为基础的一个测试框架。Junit在完成单元测试时,有些功能实现起来,还是有麻烦的 ,如重复多次,还有junit的测试中不能加入参数,使得动态交互也成为一个问题。而Jmock的出现,使测试的使用更加的方便,而且大量减少了测试代码的书写。
例子如下:
mockStmt.expect(once).method("execute").with(eq(sql)).will(returnvalue(false)); |
解释如下:
expect: 期待的执行次数,可以有 once,atLeastOnce ,notCalled 三种
method: 期待调用的方法名
with: 方法需要的参数
will: 返回值
其中的once, eq, returnvalue都是继承自MockObjectTestCase的方法。
执行后,Green,成功。尝试两次调用
assertFalse(stmt.execute(sql)); |
则提示错误。
相关的知识清参考mockObject的 资料。
4.strutsJunit对Struts的测试示例
StrtusTest是junit的扩展,使用它,不需要启动servlet容器就可以方便的测试struts应用程序(容器外测试)。它也是属于使用Mock对象测试,但是与EasyMock不同的是,EasyMock是提供了创建Mock对象的API,而StrutsTest则是专门负责测试Struts应用程序的Mock对象测试框架。除了容器外测试,StrutsTest也可以很方便的用容器内的测试。请看示例:
public class DeparmentDBActionTest extends MockStrutsTestCase {
public DeparmentDBActionTest(String arg0) { super(arg0); }
public void setUp() { super.setUp(); //指明web应用的根 File contextFile = new File("D://Projects//fog//implement//web"); setContextDirectory(contextFile); }
protected void teardown() throws Exception { super.tearDown(); }
public void testDoAdd() { //设置action的path setRequestPathInfo("[(!)]Edit");
//准备action所需要的formbean的参数 addRequestParameter("method", "doAdd"); addRequestParameter("pageId", "1"); addRequestParameter("dpname","测试部门"); addRequestParameter("dptype","测试部门"); addRequestParameter("yn","n");
//执行action actionPerform();
//验证返回的forward verifyForward("success"); } } |
有关细节,大家可以继续研究,今天主要是广而不深。
5.现有测试工具的一览性说明和大概了解
我们刚介绍的都是对于动态运行结果的测试,而对于静态代码也是存在测试工具的。下面这些只是做个介绍,有兴趣大家自己可以自己去看看:
FindBugs
PMD/CPD
Checkstyle
Jalopy/Jacobe
JDepend
JUnit
FindBugs:主要解决当前和潜伏故障,使用字节码分析来生成他的故障报告。 其中字节码分析是关键。
PMD:使用源代码分析,与编译器工作方式类似,允许你使用XPath引擎访问源代码解析器的输出。源代码分析是关键。
Checkstyle:与PMD相当相似,如名字所示,它主要是寻找格式故障方面。格式故障方面 是关键 。
Jalopy / Jacobe:帮助解决事故等待发生故障和组织故障的代码格式程序。Jalopy是开源,但是他没有出现在活跃的开发中。Jacobe 可免费使用,但不是开源。
JDepend:在你的源代码周围生成了无数个度量,包括来自于主序列的传入和传出的耦合和分配。
6.PMD对静态代码的测试
对于静态代码测试,我推荐的当然是PMD,因为我就用了这个J。请看下面的例子:
public class testSample extends TestCase { public void testMethod1() { try { int i=0; boolean b = true; assertTrue("成功",b); } catch (Exception e) { System.out.println("Yes, I catch u"); //应该到达点 } } } |
请判断 ,那些代码有错?都是格式方面很小的问题。
1) int i=0 无用,可以删除
2) testSample,类名应该大写
你现在觉得PMD怎么样,是不是就像一个程序高手J。
7.测试要注意的问题
群内讨论[微雨心情]
测试这东西很奇怪,大家都做,但是好的和坏的差别太大了。测试驱动对人的要求很高,主要是:测试的边界、测试可重复(也就是不能这次测试了下次就重写参数)。常常,在边界问题上不能很好的把握(经验),使得测试的覆盖率很低(有效性覆盖,而不是方法)。 另外就是测试参数的相互干扰和重用问题,涉及到数据的清理和为了方便测试而建立的辅助类。 而且,一般的经验不丰富的人,不容易看到问题或者很自信,所以很自然的觉得没有问题而抵触。 另外就是测试驱动的同时,还可以采用测试和原始类互为测试的方式进行开发。还有就是测试驱动时顶上和底层两边走的使用。 因为,我虽然喜欢而且坚持测试,但我也存在时间问题,以及这些问题对我来说太自信的问题。测试就是“顶”,是使用者角度对类/方法的检查,那么实现就是底。通常,这个“底”可能是多层的,而在某些情况下,我可能不完全的测试优先。这个时候,两边走和互为测试就很好。比如,我原来开发我的Brave,我一个人,为了时间进度,我就这么来。当然,我对其很自信的才会忽略测试或者暂缓。两边一起这种方式是更实际的做法。J
下面是一些具体的编写测试代码的技巧或较好的实践方法: [Java开发者]
1. 不要用TestCase的构造函数初始化Fixture,而要用setUp()和tearDown()方法。
2. 不要依赖或假定测试运行的顺序,因为JUnit利用Vector保存测试方法。所以不同的平台会按不同的顺序从Vector中取出测试方法。
3. 避免编写有副作用的TestCase。例如:如果随后的测试依赖于某些特定的交易数据,就不要提交交易数据。简单的会滚就可以了。
4. 当继承一个测试类时,记得调用父类的setUp()和tearDown()方法。
5. 将测试代码和工作代码放在一起,一边同步编译和更新。(使用Ant中有支持junit的task.)
6. 测试类和测试方法应该有一致的命名方案。如在工作类名前加上test从而形成测试类名。
7. 确保测试与时间无关,不要依赖使用过期的数据进行测试。导致在随后的维护过程中很难重现测试。
8. 如果你编写的软件面向国际市场,编写测试时要考虑国际化的因素。不要仅用母语的Locale进行测试。
9. 尽可能地利用JUnit提供地assert/fail方法以及异常处理的方法,可以使代码更为简洁。
10.测试要尽可能地小,执行速度快。