浏览 7917 次
锁定老帖子 主题:JUnit Sucks
精华帖 (2) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2007-08-05
一直以来,我写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! 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间: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都不是问题,放松放松,好好用:) |
|
返回顶楼 | |
发表时间:2007-08-05
sorry,帖子又重复了~~~~~~
|
|
返回顶楼 | |
发表时间:2007-08-05
我提个问题,如果我在一个testMethodA()中调用另一个testMetohdB(),这两个test method共用了private static Connection conn;如果此时执行testMethodA(),会对conn执行两次初始化么?
|
|
返回顶楼 | |
发表时间:2007-08-05
这样的问题都能挖掘出来,佩服。
顺道去看了遍源码才能理解问题所在, 第一个问题 没必要field Initialize啊,写一个方法初始化,放在setup里面就可以嘛。 第二个问题 不明白为什么一定要在teardown中 verify了。。 |
|
返回顶楼 | |
发表时间: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, 为什么不让框架帮忙呢? |
|
返回顶楼 | |
发表时间:2007-08-06
我也碰到过类似的IDE支持问题.
记得那时候不知道从那里抄袭到了一个Grouped Test的Junit增强,maven/ant下面跑的挺好,一上Eclipse就黄了.... |
|
返回顶楼 | |
发表时间:2007-08-07
lix23 写道 我提个问题,如果我在一个testMethodA()中调用另一个testMetohdB(),这两个test method共用了private static Connection conn;如果此时执行testMethodA(),会对conn执行两次初始化么?
静态变量在类装载的时候已经初始化了,所以应该不存在多次加载的问题. |
|
返回顶楼 | |