论坛首页 Java企业应用论坛

JUnit Sucks

浏览 7917 次
锁定老帖子 主题:JUnit Sucks
精华帖 (2) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2007-08-05  
OO
好象貌似有本书是用写一个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!


   发表时间: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都不是问题,放松放松,好好用:)

0 请登录后投票
   发表时间:2007-08-05  
sorry,帖子又重复了~~~~~~
0 请登录后投票
   发表时间:2007-08-05  
我提个问题,如果我在一个testMethodA()中调用另一个testMetohdB(),这两个test method共用了private static Connection conn;如果此时执行testMethodA(),会对conn执行两次初始化么?
1 请登录后投票
   发表时间:2007-08-05  
这样的问题都能挖掘出来,佩服。
顺道去看了遍源码才能理解问题所在,
第一个问题 没必要field Initialize啊,写一个方法初始化,放在setup里面就可以嘛。

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


0 请登录后投票
   发表时间: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, 为什么不让框架帮忙呢?
0 请登录后投票
   发表时间:2007-08-06  
我也碰到过类似的IDE支持问题.
记得那时候不知道从那里抄袭到了一个Grouped Test的Junit增强,maven/ant下面跑的挺好,一上Eclipse就黄了....
0 请登录后投票
   发表时间:2007-08-07  
lix23 写道
我提个问题,如果我在一个testMethodA()中调用另一个testMetohdB(),这两个test method共用了private static Connection conn;如果此时执行testMethodA(),会对conn执行两次初始化么?

静态变量在类装载的时候已经初始化了,所以应该不存在多次加载的问题.
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics