`
ajoo
  • 浏览: 453133 次
社区版块
存档分类
最新评论
阅读更多
好象貌似有本书是用写一个JUnit作为例子来讲解TDD的。要说TDD绝对是个好东西,不过TDD本身并不能保证搞出好软件。这不,Junit就是个活生生的例子呀。

一直以来,我写Junit+Easymock测试都是这么来的:
public class SomeTest extends TestCase {
   private final IMocksControl control = EasyMock.createStrictControl();
   private final Connection connection = control.createMock(Connection.class);
   @Override protected void tearDown() {
     control.verify();
     // other cleanups.
   }
   public void test1() {
      expect(connection.createStatement()).andReturn(null);
      ...
      control.replay();
      ...
   }
}


不要细抠具体的mock用法,我这都是临时编的。但是这个测试类有两个肥肠严重的问题。说起来我一直都是这么做的,真是愧对付给我工资的那些老板们啊!

第一个问题:

JUnit里面不应该用field initializer的——或者说,不能用构造函数来初始化test fixture的。当调用new TestSuite(SomeTest.class)的时候,JUnit系统会对每一个testSomething()函数调用一次构造函数,生成一个独立的test instance. 这样做,保证了每个测试的独立性,最大程度地避免了某个test case影响其它test case的出现。

不过,“等等”,你说。这不是正好就对了么?每个test case都会调用一遍这些field initializer,没有任何问题了呀?

是呀,所以我才这么多年一直理直气壮,厚颜无耻地用这个idiom呀,用这个idiom。直到最近,同事中的java专家告诉我,这是不安全地,不道德地,是低级趣味地!
因为JUnit在没有执行test case,只是构造test suite的时候,就调用了这些构造函数,这就造成了几个额外问题:
1。这造成TestSuite的创建不够安全,有可能出现异常——我们希望异常只出现在test case运行的时候。并且TestSuite的创建速度也有可能受到影响。
2。我们无法保证一个TestCase只被运行一次。比如说RepeatedTest就会多次运行一个TestCase。所以保证TestCase至少看上去是immutable的就比较重要了。

第二个问题:
不应该在tearDown()里面调用verify()。如果test case失败了,首先这个verify()就没有调用的必要了。不应该用tearDown()来强制调用它。其次,如果verify()也失败(相当有可能地),那么后一个exception会冲掉前面的那个(JUnit 4里面修好了这个bug),而前面那个exception才是你真正需要的呀。

而且,如果verify()失败了,那么后面的other cleanups也被跳过去了,而它们才是最最重要的呀。

你说可气不可气?我还以为JUnit这么简单的东西,肯定不会用错的呢。

但是,让我不许定义final field,让我把所有的初始化都放在setUp里面,让我在tearDown里面一个接一个地套try-finally,这,这不是代码难看的问题,这是太没人性了!

我想啊想,终于顿悟了。这不是人民内部矛盾,而是不可调和的阶级冲突啊。


JUnit不同于TestNG, 它极度强调测试的隔离性,为此不惜禁止我们在一个测试类中共享一些有用的信息(比如,一个parse好的xml数据之类),每个test method都将使用一个单独的instance。可是,如此代价换来的居然还是一个不干不嘎的局面:我们还是不能假设一个instance只被用一次。它肯定不会被两个不同的test method使用地,——但是它可以被一个test method重复使用啊。哈,哈,哈。没想到吧?

到了JUnit 4, tearDown的问题解决了。我可以在每个@After函数中释放各自的资源,框架会保证它们都被执行。不过,我们还是不能在@After的函数中调用verify(), 还是因为对于verify()我们并不希望在测试本身失败的时候还调用它。

至于构造函数问题,没有任何改善,没有。同样,JUnit 4也没有正视广大人民群众对共享数据的呼声。不管TestSetup还是@BeforeClass都是要求你用evil static field。

忍无可忍之下,我又怒了。于是自己写了一个AjooTestSuite,偷偷从TestNG搞来了几个我一直非常眼馋的annotation: @BeforeTest, @AfterTest, @BeforeSuite, @AfterSuite, @ExceptionExpected。又自己填了两个@Verify和@Shared。前者用来在测试没出问题的情况下做一个公用的verify,比如verify mock object;后者用来在test case之间共享数据。一个使用AjooTestSuite的测试类可以这么写:

public class SomeTest extends TestCase {
  public static Test suite() {
    return new AjooTestSuite(SomeTest.class);
  }
  
  // 哈哈。终于可以用final和initializer了!
  private final IMocksControl control = EasyMock.createStrictControl();
  private final Connection conn = control.createMock(Connection.class);

  @Verify
  public void verifyMocks() {
    control.verify();
  }

  @Shared
  public static XmlObject provideXml() {
    // ... read xml file and parse.
    return xmlObj;
  }
  
  private final XmlObj;

  // the shared xmlObj will be injected for each test case.
  public SomeTest(String name, XmlObject obj) {
    super(name);
    this.xmlOj = obj;
  }

  @BeforeSuite
  public static void initialize() {
    // some suite level initialization. No more TestSetup!
  }
  @AfterSuite
  public static void deinitialize() {
    // suite level deinitialization.
  }
  @BeforeTest
  public void setupMocks() {
    expect(conn.createStatement()).andReturn(null);
  }
  @ExceptionExpected(NullPointerException.class)
  public void test1() {
    throw new NullPointerException();
  }
}



写完之后,我这个得意呀。终于不用忍受JUnit屎一样的限制了。构造函数只有在测试运行的时候才调用,换句话说,这回test case是绝对immutable的了,绝对线程安全。耶!

除此之外,还加上了一些流行的annotation。@BeforeTest, @AfterTest, @BeforeSuite, @AfterSuite,@ExceptionExpected不用说,都跟TestNG一样的语义。@Verify用来搞类似verify mock之类的事情;@Shared标注的静态函数在每次test suite执行的时候会被调用一次,返回的数据会被保存并被注射给每一个test case instance。

完美呀,完美。这样一来,也不用转移到TestNG了,也不用忍受一些工具集成的问题升级到JUnit4了。就是一个新的TestSuite而已,100%向后兼容。

可是,事实证明,我说的太早了。我的java专家同事又给我泼了一瓢凉水:

现在,你在Eclipse/Intellij里面点运行,没问题,它会知道去寻找suite()函数,然后用你的自定义test suite。可是,如果你然后点某一个单独的test case, 比如test1, 然后说"run"。会发生什么哩?嘿嘿,它不再去找你的suite()函数啦,啦,啦,啦,啦(回声逐渐消失)。它直接跑到你的类里面去找这个函数来调用了。Surprise!

天啊。该死的Eclipse, 它难道不会调用suite(), 然后在suite()里面找么?

可仔细想想,又不是IDE的错。即使它调用suite()又如何?没有一个TestSuite.getTest(String name)的API供它调用啊。实际上,JUnit的TestCase也并不强制保证每个Test都有一个id的。

于是,我三天的工作白费了。呜呜呜!

总而言之,言而总之,千言万语,咬牙切齿汇成一句话:JUnit Sucks!


分享到:
评论
7 楼 liusu 2007-08-07  
lix23 写道
我提个问题,如果我在一个testMethodA()中调用另一个testMetohdB(),这两个test method共用了private static Connection conn;如果此时执行testMethodA(),会对conn执行两次初始化么?

静态变量在类装载的时候已经初始化了,所以应该不存在多次加载的问题.
6 楼 acdc 2007-08-06  
我也碰到过类似的IDE支持问题.
记得那时候不知道从那里抄袭到了一个Grouped Test的Junit增强,maven/ant下面跑的挺好,一上Eclipse就黄了....
5 楼 ajoo 2007-08-06  
关于static field evil的问题,我就不copy别人现成的分析了,看这个吧:
http://beust.com/weblog/archives/000173.html

不过,确实JUnit 4里面没有构造函数的问题了。算个大进步吧。只不过JUnit 4有俩致命问题:

1。数据共享问题。前面说过了。evil static field。
2。JUnit 4和JUnit 3基本上是两个不同的框架了。向后兼容性很差。如果我要用JUnit 4, 舍得放弃现有的所有为JUnit 3编写的工具(我们公司就有一大堆这种工具类,比如XYZTestSuite什么的),那么我为什么不直接上TestNG呢?

至于说setup没问题,verify()为什么想放在tearDown, 这就是个人口味问题了。
我就觉得:
private final Map map = new HashMap();
private final List list = new ArrayList();
// test cases that use map and list
...

private final IMocksControl control = EasyMock.createStrictControl();
private final Connection connection = control.createMock(Connection.class);
// test cases that use connection
...


private Map map;
private List list;
// test cases that use map and list
...

private IMocksControl control;
private Connection connection;
// test cases that use connection
...


protected void setUp() {
  map = new HashMap();
  List = new ArrayList();
  control = EasyMock.createStrictControl();
  connection = control.createMock(Connection.class);
}


要舒服得太多。用field initializer,可以final,声明和初始化可以放在一起。而且每组相关的field可以放在一起,和使用它们的测试代码紧挨着,而不相关的组可以分开老远老远。这样更容易读。
用setUp, 写一个field, 还要记得在30公里开外的地方那个setUp()里面写初始化。而所有的初始化,不管搭嘎不搭嘎,都要堆在一起。

至于verify()嘛,我发现很多时候我要在所有的test method结束的地方写control.verify(),这多重复啊?如果知道每个test method结束都要verify, 为什么不让框架帮忙呢?
4 楼 Godlikeme 2007-08-05  
这样的问题都能挖掘出来,佩服。
顺道去看了遍源码才能理解问题所在,
第一个问题 没必要field Initialize啊,写一个方法初始化,放在setup里面就可以嘛。

第二个问题 不明白为什么一定要在teardown中 verify了。。


3 楼 lix23 2007-08-05  
我提个问题,如果我在一个testMethodA()中调用另一个testMetohdB(),这两个test method共用了private static Connection conn;如果此时执行testMethodA(),会对conn执行两次初始化么?
2 楼 birdjavaeye 2007-08-05  
sorry,帖子又重复了~~~~~~
1 楼 birdjavaeye 2007-08-05  
测试中static field不怎么evil啊,平时测试我没遇到任何问题
JUnit4中tear down都ok了,eclipse/intellij都支持。如果“一些工具集成”对Junit4不友好,那基本也对TestNG和AjooTestSuite不友好了,这样的工具也得淘汰了

对于数据共享,也没什么问题,至少对Junit4而言(Juni3许久没用记不清了)
即可以是field initializer也可以在构造函数里创建,final也可以。
没发现在test case启动前就初始化这些field的情况
保证test case所谓immutable也很简单

就是说,你提到的问题在Junit4都不是问题,放松放松,好好用:)

相关推荐

    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的jar包

    Files contained in junit4-4.8.2.jar: LICENSE.txt META-INF/MANIFEST.MF junit.extensions.ActiveTestSuite.class junit.extensions.RepeatedTest.class junit.extensions.TestDecorator.class junit.extensions...

    junit5.jar

    这个名为"junit5.jar"的文件正是JUnit 5的核心库,它包含了执行单元测试所需的所有核心组件,包括JUnit Platform、JUnit Jupiter和JUnit Vintage。本文将全面解析JUnit 5的关键特性和使用方法。 首先,JUnit ...

    junit4.1 junit4.1

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

    JUnit in Action 3nd Edition

    "JUnit in Action 3rd Edition" JUnit是一种流行的Java单元测试框架,由Kent Beck和Eric Gamma于1997年创立。JUnit在软件测试领域中扮演着重要的角色,帮助开发者编写高质量的代码。下面是关于JUnit的重要知识点: ...

    JUnit API JUnit API

    JUnit API JUnit API JUnit API JUnit API JUnit API

    Junit5依赖整合包

    Junit5是Java开发中最常用的单元测试框架之一,它的出现为开发者提供了更加高效、灵活的测试体验。相较于之前的版本,Junit5引入了许多新的特性和改进,使得测试代码的编写和维护变得更加简单。本整合包包含了Junit5...

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

    JUnit是Java编程语言中最常用的单元测试框架之一,它允许开发者编写可重复运行的测试用例,以确保代码的正确性和稳定性。JUnit3.8.1是该框架的一个较早版本,尽管现在已经有更新的版本(如JUnit5),但了解其基本...

    Junit5.7.2离线jar

    JUnit是Java编程语言中最常用的单元测试框架之一,用于编写和运行可重复的自动化测试用例。Junit5.7.2版本是这个框架的一个稳定版本,提供了许多改进和新特性,使得测试更加高效且易于维护。这个离线jar文件包含了...

    Junit5.zip

    JUnit5是Java编程语言中最流行的单元测试框架之一,它的最新版本带来了许多改进和新特性,使得测试更加高效和灵活。本资源包含的`junit5.jar`是JUnit5的运行库,可以用于运行使用JUnit5编写的测试用例。而`junit5-...

    junit-jupiter-5.5.1.rar

    JUnit5相关的 JUnit Jupiter.jar依赖包,主要包含: junit-jupiter-5.5.1.jar,junit-jupiter-api-5.5.1.jar,junit-jupiter-engine-5.5.1.jar,junit-jupiter-params-5.5.1.jar,junit-jupiter-api-5.4.2.jar,...

    Junit5.jar包,代码测试工具

    JUnit5是Java编程语言中最广泛使用的单元测试框架之一,它为开发者提供了强大的测试能力,确保代码的质量和稳定性。本篇文章将详细介绍JUnit5的核心组件、主要功能以及如何在项目中使用。 JUnit5由三个主要模块组成...

    JUnit in action JUnit Recipies

    《JUnit in Action》和《JUnit Recipes》是两本关于Java单元测试的重要书籍,它们深入浅出地介绍了如何使用JUnit框架进行高效、可靠的测试。JUnit是一个流行的开源测试框架,广泛用于Java应用程序的单元测试,它提供...

    junit-4.12.jar下载

    JUnit是Java编程语言中最常用的单元测试框架之一,它允许开发者编写可执行的测试用例来验证代码的功能。这里我们关注的是`junit-4.12.jar`版本,这是一个重要的里程碑,因为它带来了许多改进和新特性。让我们深入...

    junit工具jar包

    JUnit是Java编程语言中最常用的单元测试框架之一,它允许开发者编写可执行的测试用例来验证代码的功能。这个“junit工具jar包”是JUnit 4.8.1版本,一个稳定且广泛使用的版本,提供了丰富的断言方法和测试注解,便于...

    junit测试_java_JUnit_JUnit测试_

    JUnit是Java编程语言中最常用的单元测试框架之一,它允许开发者编写可执行的测试用例来验证代码的功能。单元测试是对程序中的最小可测试部分——通常是一个函数或方法——进行检查,确保其按照预期工作。JUnit作为...

    Junit4.12和依赖包

    JUnit是Java编程语言中最常用的单元测试框架之一,它极大地简化了对代码进行验证和调试的过程。Junit4.12是该框架的一个稳定版本,它包含了许多改进和新特性,使得测试更加灵活和强大。在本文中,我们将深入探讨...

    junit4 jar包

    JUnit4是Java编程语言中最广泛使用的单元测试框架之一,它为开发者提供了编写可重复执行、易于维护的测试代码的能力。这个“junit4 jar包”包含了运行JUnit4测试所必需的库文件,主要包括两个核心组件:`junit-4.11....

    junit5-r5.5.2.zip

    JUnit 5是JUnit系列的最新版本,它引入了模块化设计,包括JUnit Platform、JUnit Jupiter和JUnit Vintage三个核心组件。JUnit Platform提供了运行测试的基础架构,JUnit Jupiter则包含编写和执行测试的核心API,而...

    junit-4.11-API文档-中文版.zip

    赠送jar包:junit-4.11.jar; 赠送原API文档:junit-4.11-javadoc.jar; 赠送源代码:junit-4.11-sources.jar; 赠送Maven依赖信息文件:junit-4.11.pom; 包含翻译后的API文档:junit-4.11-javadoc-API文档-中文...

Global site tag (gtag.js) - Google Analytics