`
icomparator
  • 浏览: 18568 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

junit设计理念与工作原理(转自selfishman的博客)

 
阅读更多
      junit设计理念与工作原理
概述:
        JUnit是由 Erich Gamma 和 Kent Beck 编写的一个回归测试框架(regression testing framework),用于帮助Java开发人员编写单元测试。 所谓单元测试也就是白盒测试。单元测试在xp社区极为流行,作为测试驱动开发,junit是java开发使用最为广泛的框架。该框架也得到了绝大多数java IDE和其他工具(例如,ant)的集成支持。同时,junit还有很多的第三方扩展和增强包可供使用。junit最近的变化比较少,现在的最高版本仍是3.8.1。
    JUnit的使用很简单,关于JUnit使用的文章和例子也很多,本文着重讲解JUnit的基本概念和其设计理念和工作原理。
 
一.junit基本概念
   JUnit有几个基本概念:TestCase,TestSuite,TestFixtrue。
1.TestCase
    代表一个测试用例,每一个TestCase实例都对应一个测试,这个测试通过这个TestCase实例的名字标志,以便在测试结果中指明哪个测试出现了问题。
2.TestSuite
    代表需要测试的一组测试用例。
3.TestFixtrue
    TestFixtrue代表一个测试环境。它用于组合一组测试用例,这组测试用例需要共同的测试运行环境。
 
二.junit的设计
    JUnit的核心是围绕命令模式和组合模式设计的,当然同时使用了模版方法模式,参数收集模式,适配器模式等。这里只是简单介绍。
    JUnit框架中有几个核心的接口和类。
1.Test接口
    代表一个测试。它是框架的主接口有两个方法:
    int countTestCases();//返回所有测试用例的个数。
    void run(TestResult result);//运行一个测试,并且收集运行结果到TestResult。
2.TestCase类
    TestCase实现了Test接口,是框架提供的供我们继承的类,我们的所有的测试方法都需要在TestCase的子类中定义,并且符合特定的设计协议。
    一个TestCase实例代表一个具体的测试实例,对应一个对某一方法或概念的测试。每个TestCase实例都有一个名字。
    一个TestCase类却定义了一个TestFixture。
    具体的说就是我们自己定义的TestCase子类中可以定义很多的public 没有参数的 testxxx方法。运行时,每个testxxx都在自己的fixture中运行。每个运行的TestCase都有一个名字,如果不指定,一般是TestCase中定义的test方法的名字。
3.TestSuite类
    和TestCase一样TestSuite也实现了Test接口。一个TestSuite可以包含一系列的TestCase。把testCase组装入TestSuite有几种方式:A,通过将TestCase的Class参数传入TestSuite的构造函数,TestSuite会自动收集TestCase中所有的public的没有参数的testxxx方法加入TestSuite中。
B,构造空的TestSuite后通过void addTest(Test test)方法添加测试。
C:构造空的TestSuite后通过void addTestSuite(Class testClass) 方法添加测试集。
4.TestResult类
    主要通过runProtected方法运行测试并收集所有运行结果。
5.TestRunner类
    启动测试的主类,我们可以通过直接调用它运行测试用例,IDE和其他一些工具一般也通过这个接口集成JUnit。
6.Assert类
    用于断言,TestCase继承自该类,我们的测试方法通过这些断言判断程序功能是否通过测试。
7.TestListener接口
    测试运行监听器,通过事件机制处理测试中产生的事件,主要用于测试结果的收集。
    以上是框架的核心接口和类的介绍,通过上面的介绍我们很容易看出来Test,TestCase和TestSuite的设计采用了Composite模式。这样JUnit可以一次运行一个测试用例,也可以一次运行多个测试用例,TestRunner只关心Test接口,而对运行的是单个的TestCase还是同时运行多个TestCase并不在意。
    TestCase还使用了Template Method模式:

public void run() { 
    setUp(); 
    runTest(); 
    tearDown(); 
}protected void runTest() { 
}

protected void setUp() { 
}

protected void tearDown() { 
}

    JUnit同时使用了Command模式,对于典型的Command模式一般有5种角色:
1)        命令角色(Command):声明执行操作的接口。有java接口或者抽象类来实现。
2)        具体命令角色(Concrete Command):将一个接收者对象绑定于一个动作;调用接收者相应的操作,以实现命令角色声明的执行操作的接口。
3)        客户角色(Client):创建一个具体命令对象(并可以设定它的接收者)。
4)        请求者角色(Invoker):调用命令对象执行这个请求。
5)        接收者角色(Receiver):知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者。
    对于JUnit的设计,不能明显的区分出这5种角色,因为它的设计相对复杂,同时参杂了其他模式。
    Test接口可以认为是命令模式中的命令角色Command接口,void run(TestResult result)接口方法定义了需要执行的操作;TestCase可以看作是具体命令角色,但又不全是,因为我们还需要自己通过继承TestCase类定义测试方法,这样的每一个测试方法都回被包装在一个TestCase实例中。TestResult可以看作请求者角色(Invoker),它会通过protected void run(final TestCase test) 运行测试并收集结果。我们自己写的Test方法可以认为是接收者角色(Receiver),因为我们的方法才具体执行这个命令。TestRunner就是客户角色(Client),它通过TestResult result= createTestResult()构造TestResult,并通过suite.run(result)运行测试用例(suite是一个Test接口的具体实例,可以是TestCase也可以是TestSuite,但客户端不关心它是什么,这就是组合模式的好处。同时,suite.run(result)又调用result.run(test),如果不仔细分析,就会被这种设计搞迷惑)。
 
 
三.junit的工作原理
    知其然还要知其所以然,JUnit使用比较简单,但为什么要这样使用,什么是best practices,就需要了解JUnit的内部工作原理了。
    一个简单的测试的例子:
/**
 * A sample test case, testing <code>java.util.Vector</code>.
 *
 */
public class VectorTest extends TestCase {
 protected Vector fEmpty;
 protected Vector fFull;
 public static void main (String[] args) {
  junit.textui.TestRunner.run (suite());
 }
 protected void setUp() {
  fEmpty= new Vector();
  fFull= new Vector();
  fFull.addElement(new Integer(1));
  fFull.addElement(new Integer(2));
  fFull.addElement(new Integer(3));
 }
 public static Test suite() {
  return new TestSuite(VectorTest.class);
 }
 public void testCapacity() {
  int size= fFull.size(); 
  for (int i= 0; i < 100; i++)
   fFull.addElement(new Integer(i));
  assertTrue(fFull.size() == 100+size);
 }
 public void testClone() {
  Vector clone= (Vector)fFull.clone(); 
  assertTrue(clone.size() == fFull.size());
  assertTrue(clone.contains(new Integer(1)));
 }
 public void testContains() {
  assertTrue(fFull.contains(new Integer(1)));  
  assertTrue(!fEmpty.contains(new Integer(1)));
 }
 public void testElementAt() {
  Integer i= (Integer)fFull.elementAt(0);
  assertTrue(i.intValue() == 1);
  try { 
   fFull.elementAt(fFull.size());
  } catch (ArrayIndexOutOfBoundsException e) {
   return;
  }
  fail("Should raise an ArrayIndexOutOfBoundsException");
 }
 public void testRemoveAll() {
  fFull.removeAllElements();
  fEmpty.removeAllElements();
  assertTrue(fFull.isEmpty());
  assertTrue(fEmpty.isEmpty()); 
 }
 public void testRemoveElement() {
  fFull.removeElement(new Integer(3));
  assertTrue(!fFull.contains(new Integer(3)) ); 
 }
}
 
1. 主入口。
public static void main (String[] args) {
  junit.textui.TestRunner.run (suite());
 }
 
    调用了TestRunner.run(Test)方法,该方法执行的操作的结果就是构造TestResult,然后调用TestResult.run(Test)方法。
    当然,TestRunner也有main方法,可以在命令行下执行,main方法执行的结果和TestRunner.run 相似。
2.suite()
     public static Test suite() {
          return new TestSuite(VectorTest.class);
     }  
    这是JUnit框架使用TestSuite规定的模式,尤其是在命令行或图形界面下,只有定义了public static Test suite() 方法,框架才会按照我们的定义运行框架。
3.new TestSuite(VectorTest.class)
    TestSuite有三个构造函数一个没有参数,一个以Class为参数还有一个以Class和名字字符串作为参数。Class必须是实现了Test接口的子类,一般继承自TestCase,并且该类中定义了以Test开头没有参数的测试方法。
//构造函数
  while (Test.class.isAssignableFrom(superClass)) {
   Method[] methods= superClass.getDeclaredMethods();
   for (int i= 0; i < methods.length; i++) {
    addTestMethod(methods[i], names, theClass);
   }
   superClass= superClass.getSuperclass();
  }
//addTestMethod
  if (! isPublicTestMethod(m)) {
   if (isTestMethod(m))
    addTest(warning("Test method isn't public: "+m.getName()));
   return;
  }
  names.addElement(name);
  addTest(createTest(theClass, name));
//isTestMethod
 private boolean isTestMethod(Method m) {
  String name= m.getName();
  Class[] parameters= m.getParameterTypes();
  Class returnType= m.getReturnType();
  return parameters.length == 0 && name.startsWith("test") && returnType.equals(Void.TYPE);
  }
    所以,我们必须保证我们要在实现Test接口的子类中定义符合以上要求的测试类,否则,框架将不会运行我们的测试方法。
    同时我们也看到,当我们将Test类传入TestSuite后,TestSuite将所有的test构造为TestCase实例,每个实例都会有一个名字,和要测试的方法。
//createTest  (addTest(createTest(theClass, name))时调用)
 static public Test createTest(Class theClass, String name) {
  Constructor constructor;
  try {
   constructor= getTestConstructor(theClass);
  } catch (NoSuchMethodException e) {
   return warning("Class "+theClass.getName()+" has no public constructor TestCase(String name) or TestCase()");
  }
  Object test;
  try {
   if (constructor.getParameterTypes().length == 0) {
    test= constructor.newInstance(new Object[0]);
    if (test instanceof TestCase)
     ((TestCase) test).setName(name);
   } else {
    test= constructor.newInstance(new Object[]{name});
   }
  } catch (InstantiationException e) {
   return(warning("Cannot instantiate test case: "+name+" ("+exceptionToString(e)+")"));
  } 
  return (Test) test;
 }
    在createTest(Class theClass, String name)方法中theClass是实现了Test接口的类,一般情况下是TestCase的子类。这样name就是TestCase的名字,用于标志一个测试。
    在TestCase的protected void runTest() throws Throwable 方法中:
runMethod= getClass().getMethod(fName, null);//获取方法名
runMethod.invoke(this, new Class[0]);//运行测试方法。
这也就是上面说的,一个TestCase类中可以定义很多test方法,但一个TestCase实例只对应一个测试方法。
4.运行
    Test.run(TestResult)是实际上调用TestResult.run(Test)。
//TestResult.run(Test)
 protected void run(final TestCase test) {
  startTest(test);
  Protectable p= new Protectable() {
   public void protect() throws Throwable {
    test.runBare();
   }
  };
  runProtected(test, p);
 
  endTest(test);
 }
该方法也是一个模版方法,关键是runProtected方法:
 public void runProtected(final Test test, Protectable p) {
  try {
   p.protect();
  } 
  catch (AssertionFailedError e) {
   addFailure(test, e);
  }
  catch (ThreadDeath e) { // don't catch ThreadDeath by accident
   throw e;
  }
  catch (Throwable e) {
   addError(test, e);
  }
 }
这个方法起到的作用就是当测试出现异常或错误时会在TestResult中记录,如果是ThreadDeath 就继续抛出异常,程序可能会终止,如果是其他错误和异常,程序将继续运行。这也就是protect的含义。因此,我们抛出Assert中的异常时,不会影响下面的测试继续运行。
    这个方法中传入的Protectable p的protect方法调用了test.runBare();我们看到,TestRunner调用了Test的run(TestResult)方法后,TestResult实际上是在一个保护的环境下调用TestCase的runBase方法。也就是说如果我们自己的测试类没有继承TestCase,也要定义一个runBase方法,执行基本的操作。基于这一点,我们的测试类应该继承自TestCase。
5.一个比较绕的地方
    TestSuite也是一个Test,也有我们的例子中是调用TestSuite的void run(TestResult result)方法。
 public void run(TestResult result) {
  for (Enumeration e= tests(); e.hasMoreElements(); ) {
     if (result.shouldStop() )
      break;
   Test test= (Test)e.nextElement();
   runTest(test, result);
  }
 }
 
 public void runTest(Test test, TestResult result) {
  test.run(result);
 }
    上面的代码首先是一个循环,其次看起来好像形成了一个环:run(TestResult result) -〉runTest(test, result)-〉test.run(result)-〉run(TestResult result),这就要求我们必须对java的运行时类型比较熟悉才能比较容易的理解。
    tests()是在TestSuite构造时构造的所有的tests,一般情况下这些test都是TestCase子类的实例。不过也有可能是另一个TestSuite,比如通过 public void addTestSuite(Class testClass) 方法提阿加:
    public void addTestSuite(Class testClass) {
      addTest(new TestSuite(testClass));
    }
 
    如果test是TestCase,就直接调用按照4中说明的方法运行。如果是TestSuite就会递归调用TestSuite中定义的void run(TestResult result)方法,直到没有了suite中的所有的Test都不是suite(因为suite在构造时就会将suite解析为单个的TestCase的集合,可以通过tests()方法取出所有的test)。
    这里不太容易说清楚,仔细看看代码思考一下就明白了。
 
总结:
    以上介绍了JUnit的基本概念和设计理念及工作原理。junit当然也有很多不尽人意的地方。不过由于它是java中使用的最广泛的单元测试框架,我们还是需要仔细研究一下的。
分享到:
评论

相关推荐

    Junit单元测试内部机制解析

    JUnit的核心设计理念是将测试逻辑与业务逻辑分离,通过一系列标准化的API实现测试的自动化执行、结果收集和报告生成。下面我们将深入探讨JUnit的内部机制。 ##### 1. **JUnit测试执行流程** JUnit的测试执行流程...

    Junit设计模式分析

    Junit作为Java开发中的主流单元测试工具,其内在的设计理念与多种设计模式密切相关。 1. **工厂模式**:在Junit中,测试用例的创建通常涉及工厂模式。测试类是测试用例的工厂,它负责创建被测试对象,确保测试环境...

    JUnit设计模式分析

    本篇将深入分析JUnit源码中的设计模式,帮助你理解其内在的架构原理,提升你的编程技能。 首先,JUnit的核心设计原则之一是“开闭原则”(Open-Closed Principle),它主张软件实体(类、模块、函数等)应对于扩展...

    Junit设计模式分析(带源码)

    通过深入分析JUnit的源码,开发者不仅可以了解其内部工作原理,还能学习到如何利用设计模式优化自己的代码和测试。同时,掌握JUnit的高级特性和与其他工具的集成方式,将极大地提升开发效率和软件质量。

    JUnit的框架设计及其使用的设计模式

    首先,JUnit的核心设计理念是“简单易用”。它遵循了“最小化API”原则,提供了一套简洁的注解(如@Test、@Before、@After等)来标记测试方法和设置,让开发者无需编写复杂的初始化和清理代码即可创建测试用例。 ...

    一个参考Junit的设计模式ppt

    JUnit作为一个强大的单元测试框架,它的设计理念、核心特性和背后的优秀设计模式,对Java开发人员来说是不可或缺的工具。通过理解和熟练运用JUnit,开发者可以更有效地进行测试,提升代码质量,降低维护成本,从而...

    自动饮料机Junit测试(软件测试与质量保证实验).rar

    Junit提供断言机制,用于验证预期结果是否与实际结果相符,以及注解(如@Test)来标识哪些方法是测试方法。 **单元测试的基本原则** 1. **独立性**:每个测试用例应独立于其他用例,不依赖于测试执行的顺序。 2. *...

    Junit设计模式应用

    《Junit设计模式应用》是基于作者业余时间的翻译成果,旨在通过设计模式的角度深入剖析JUnit的内在原理,以此促进读者对单元测试框架理解和运用能力的提升。设计模式是软件工程中的宝贵经验总结,它为解决常见问题...

    JUnit设计模式分析及简化的JUnit代码

    首先,JUnit的核心设计理念之一是“依赖注入”,这是设计模式中的一个关键概念。依赖注入允许测试类动态地获取它们所依赖的对象,而不是直接创建它们。这提高了代码的灵活性和可测试性,因为测试环境可以提供模拟的...

    JUnit与Ant教程

    JUnit与Ant教程JUnit与Ant教程JUnit与Ant教程JUnit与Ant教程JUnit与Ant教程

    junit5.rar包含JUnit Platform + JUnit Jupiter + JUnit Vintage依赖jar包

    JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage,包含依赖包:junit-jupiter-5.5.1.jar,junit-jupiter-engine-5.5.1.jar,junit-jupiter-params-5.5.1.jar,junit-platform-launcher-1.5.1.jar,junit-...

    junit源码以及牵涉到的设计模式

    JUnit作为一款广泛应用于Java项目的单元测试框架,其设计理念和实现方式对于软件开发者来说具有很高的学习价值。本文将深入探讨JUnit源码,并重点关注其中使用的设计模式,这些设计模式不仅提高了JUnit的可扩展性和...

    junit4.1 junit4.1

    junit4.1junit4.1junit4.1junit4.1junit4.1

    JUnit3.8.1 以及使用JUnit测试的代码demo

    在JUnit3.8.1中,测试类需要继承自`junit.framework.TestCase`。测试方法通常以`test`开头,例如`testAdd()`,并且它们不需要返回值,也不接受参数。 在JUnit中,使用注解来标记测试方法。在JUnit3.8.1中,这些注解...

    JUnit API JUnit API

    JUnit API JUnit API JUnit API JUnit API JUnit API

    JUnit in action JUnit Recipies

    JUnit提供了注解(Annotations)如@Test,@Before,@After等,这些注解用于标记测试方法、测试前的准备工作和测试后的清理工作。例如,@Test注解标识了一个测试方法,当运行测试类时,这个方法会被自动执行。@Before...

Global site tag (gtag.js) - Google Analytics