自作聪明的junit.swingui.TestRunner

原创 2005年04月29日 10:48:00

问题
在junit.swingui.TestRunner的时候发现TestRunner启动过程中报错:
log4j:ERROR A "org.apache.log4j.ConsoleAppender" object is not assignable to a "org.apache.log4j.Appender" variable.
同时也发现一个平时工作正常的类在使用junit.swingui.TestRunner进行测试的时候报告一个奇怪的 ClassCastException,明明构造的对象的类是实现了指定的接口的,可是就是无法造型到接口上。
进一步研究发现,即使造型回原来的类也不行,虽然调试的时候显示构造的对象就是指定的类,但是就是无法造型成这个类,一度认为是妖人作祟或者机子被落了降头。

研究
求得庄老大再次出手,一下指出指出问题在于不同的类装载器装载的类无法相互造型的。于是进去junit.swingui.TestRunner里面去找类装载器,一翻折腾之后终于找到:

junit.runner.BaseTestRunner
           |------junit.swingui.TestRunner
           |------junit.textui.TestRunner

在BaseTestRunner里面定义了这样一个方法:
    public TestSuiteLoader getLoader() {
        if (useReloadingTestSuiteLoader())
            return new ReloadingTestSuiteLoader();
        return new StandardTestSuiteLoader();
    }
不过注意到junit.textui.TestRunner是不会出上面的错误的,因为它自己重载了getLoader()方法,
    /**
     * Always use the StandardTestSuiteLoader. Overridden from
     * BaseTestRunner.
     */
    public TestSuiteLoader getLoader() {
        return new StandardTestSuiteLoader();
    }
但是junit.swingui.TestRunner就很自作聪明了,为了避免每次在点“run”按钮的时候装载运行器本身,就直接使用了基类的方法去获取装载器),这样基类就可以调用自己的getLoader方法来决定要启用那个classloader:

    public TestSuiteLoader getLoader() {
        if (useReloadingTestSuiteLoader())
            return new ReloadingTestSuiteLoader();
        return new StandardTestSuiteLoader();
    }

如果我们用sun的jdk的话,这个方法会返回一个TestCaseClassLoader对象,而这个对象在装载class的时候总是调用creatLoader方法:
    protected TestCaseClassLoader createLoader() {
        return new TestCaseClassLoader();
    }

返回的其实是TestCaseClassLoader。这样如果被测试类使用了log4j的话,会造成org.apache.log4j.Appender类被 sun.misc.Launcher$AppClassLoader(也就是sun.misc.Launcher类的嵌入类AppClassLoader)装载一次(在启动test的过程中vm自动装载被引用到的类),然后在运行的时候又被junit.runner.TestCaseClassLoader再装载一次。由两个装载器装载进来的类不管是不是来自同一个.class文件,都会被认为是两个不同的类。因此就造成了上面的错误。
同样的,如果你在自己的代码里面这样装载类:
MyClass myClass = (MyClass)Thread.currentThread().getContextClassLoader().loadClass(mClassName);
也会造成相同的问题并抛出ClassCastException。因为MyClass是在运行测试的过程由junit.runner.TestCaseClassLoader装载的,而Thread.currentThread().getContextClassLoader()却指向的是sun.misc.Launcher$AppClassLoader。

解决方法
1 java -Dlog4j.ignoreTCL junit.swingui.TestRunner
我猜TCL是ThreadClassLoader的缩写,这个参数的意思大概就是让log4j忽略Thread自己的类装载器(sun.misc.Launcher$AppClassLoader),改而使用当前Class的装载器(junit.runner.TestCaseClassLoader)来装载。但是这个方法只能解决log4j的错误报告(改变了org.apache.log4j.ConsoleAppender的装载方式),但是对我们自己写的代码中的问题却没有作用。

2 在我们自己的类里面写上一段静态代码:
  static{
        Thread.currentThread().setContextClassLoader(MyClassFactory.class.getClassLoader());
  }
和方法一类似,这也是在工厂类中用加载了当前lass的装载器(TestCaseClassLoader)来代替Thread的初始化装载器sun.misc.Launcher$AppClassLoader。这个方法可以解决我们自己代码中的问题,并且不会带来影响原来的其他代码。结合第一种方法可以解决上面的两个问题。但是如果你有好几个工厂类,或者你用的其他包里面用了这样的装载方式……那你还可以试试下面的偏门:

3 注意到BaseTestRunner要进行一个useReloadingTestSuiteLoader()判断才决定返回哪个装载器
public TestSuiteLoader getLoader() {
    if (useReloadingTestSuiteLoader())
        return new ReloadingTestSuiteLoader();
    return new StandardTestSuiteLoader();
}
我们来看看这个判断过程:
protected boolean useReloadingTestSuiteLoader() {
    return getPreference("loading").equals("true") && !inVAJava() && fLoading;
}
嗯,里面有个inVAJava()是什么玩意儿?
public static boolean inVAJava() {
    try {
        Class.forName("com.ibm.uvm.tools.DebugSupport");
    }
    catch (Exception e) {
        return false;
    }
    return true;
}
原来它是想判断如果当前使用的是ibm的虚拟机就使用默认装载器,但是判断的条件也忒简单了点,很容易就吧它给蒙过去了:
在当前工程下创建com.ibm.uvm.tools包,在其中创建DebugSupport类:
package com.ibm.uvm.tools;
public class DebugSupport{}
没有错,就这个空白的类,这样就可以把junit.swingui.TestRunner给蒙倒。这样做据说的副作用是,每次点run按钮的时候,都要重起gui环境,但是我没有发现有什么区别。不过要是没有区别,人家又干吗费那么多事呢?不解。

4 excluded.properties

以前没有注意到,junit有一个配置文件可以配置不需要用TestCaseClassLoader装载的包和类,多亏网友linux_china提醒,现在有了第四个解决方法:

在工程下建立junit.runner包,然后在包里面放 excluded.properties文件,内容象这样:

#
# The list of excluded package paths for the TestCaseClassLoader
#
excluded.0=sun.*
excluded.1=com.sun.*
excluded.2=org.omg.*
excluded.3=javax.*
excluded.4=sunw.*
excluded.5=java.*
excluded.6=org.w3c.dom.*
excluded.7=org.xml.sax.*
excluded.8=net.jini.*
excluded.9=org.apache.log4j.*
excluded.10=emu.*
......

这样junit读到这些包的时候就不会用TestCaseClassLoader去装载他们了。

参考资料

http://mail-archives.apache.org/mod_mbox/logging-log4j-user/200301.mbox/%3C3E1F1A31.2000605@attbi.com%3E

author: emu(黄希彤)

java.sql.Statement的讲解

public interface Statement Statement 对象用于执行一条静态的 SQL 语句并获取它产生的结果。 任何时侯每条语句仅能打开一个 ResultSet 。因此,如果对...

简介:SmallSQL ——纯Java轻型关系型数据库

SmallSQL是一个100%纯Java的DBMS,一个用于Java桌面应用程序的嵌入式关系型数据库。它有一个JDBC3.0接口并提供许多ANSI SQL92和ANSI SQL99特性。SmallSQ...

junit开源测试框架学习笔记一探寻junit基本组件TestCase、TestSuite、TestRunner和TestListener

还记得培训半路出家来搞JAVA的时候,老师讲的第一个开源框架就是junit,当时觉得是还挺好用的,在myeclipse中右键单击一下,对于按照juni规定格式的方法就会运行,并判断是否和断言也就是你预...

soapui中的testrunner.bat调研姿势,用于自动化测试

soapui中的testrunner.bat调研姿势,用于自动化测试 副标题:soapui基于持续集成工具自动化运行的调研姿势 各位亲爱的同仁们,大家好吗? 最近项目在搞持续集成工具,我们的测试...
  • wanglha
  • wanglha
  • 2015年04月29日 16:15
  • 1186

Android测试四----TestRunner源码分析。

通过JUnit,我们可以指定需要运行的测试用例。 public static Test suite() { suite.addTest(new MathTest("testAdd"))...
  • nitibu
  • nitibu
  • 2015年09月10日 16:18
  • 382

我自作聪明,擅自主张的优化了jQuery选择器。

闲了一会,写篇博客。 最近项目里面,我自作聪明,擅自主张的,自我感觉的优化了一下jQuery选择器。哈哈哈~~~~ ( ﹁ ﹁ ) 我这么想的: jQuery最底层采用getById getByT...

自作聪明的开发

近日查看数据库运行较长的语句,发现我们这边的开发人员真是厉害,不懂装懂的本领真高。 开发以为只要走索引就是快的,而且刚好知道index hint可以强制走索引,所以就用上了。可是走的是bit...

junit-4.12.jar

  • 2017年12月08日 17:14
  • 272KB
  • 下载

junit-4.8.2

  • 2017年11月22日 01:27
  • 203KB
  • 下载
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:自作聪明的junit.swingui.TestRunner
举报原因:
原因补充:

(最多只允许输入30个字)