【问题标题】:Using reflection to change static final File.separatorChar for unit testing?使用反射更改静态最终 File.separatorChar 进行单元测试?
【发布时间】:2011-01-29 06:18:15
【问题描述】:

具体来说,我正在尝试为需要使用File.separatorChar 在 Windows 和 unix 上构建路径的方法创建单元测试。代码必须在两个平台上运行,但是当我尝试更改这个静态 final 字段时,我遇到了 JUnit 错误。

有人知道发生了什么吗?

Field field = java.io.File.class.getDeclaredField( "separatorChar" );
field.setAccessible(true);
field.setChar(java.io.File.class,'/');

当我这样做时,我得到

IllegalAccessException: Can not set static final char field java.io.File.separatorChar to java.lang.Character

想法?

【问题讨论】:

  • 最好在其他目标操作系统下的 VirtualBox 环境中运行单元测试。谁知道当你像这样弄乱 JVM 时会发生什么。此外,也许可以重写您的代码以不直接使用 File.separatorChar。例如,您可以使用 File(parentFile, name) 构造函数构建路径。
  • @Thilo:这是个好主意,现在我想起来了,可能有一种方法可以跨平台运行我的逻辑,而无需处理文件 URL。但是,我认为知道如何更改 java.io.File.separatorChar 对于其他一些合法用例来说是一件有用的事情。

标签: java unit-testing reflection file-io


【解决方案1】:

来自Field.set 的文档:

如果基础字段是最终字段,则该方法将抛出 IllegalAccessException,除非 setAccessible(true) 已成功处理此字段并且此字段是非静态的

所以一开始你似乎不走运,因为File.separatorCharstatic。令人惊讶的是,一种解决此问题的方法:只需通过反射使 static 字段不再是 final

我改编了这个解决方案from javaspecialist.eu

static void setFinalStatic(Field field, Object newValue) throws Exception {
    field.setAccessible(true);

    // remove final modifier from field
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

    field.set(null, newValue);
}

我已经测试过了,它可以工作:

setFinalStatic(File.class.getField("separatorChar"), '#');
System.out.println(File.separatorChar); // prints "#"

对这种技术要格外小心。撇开毁灭性的后果不谈,以下实际上是有效的:

setFinalStatic(Boolean.class.getField("FALSE"), true);
System.out.format("Everything is %s", false); // "Everything is true"

重要更新:上述解决方案并非适用于所有情况。如果该字段在重置之前可以访问并通过反射读取,则会引发IllegalAccessException。它失败是因为反射 API 创建了内部 FieldAccessor 对象,这些对象被缓存和重用(请参阅 java.lang.reflect.Field#acquireFieldAccessor(boolean) 实现)。 失败的示例测试代码:

Field f = File.class.getField("separatorChar"); f.setAccessible(true); f.get(null);
// call setFinalStatic as before: throws IllegalAccessException

【讨论】:

  • #define FALSE 1 又回来了,它已经准备好重新开始。
  • 天哪! setAccessible 的引入是个大问题。
  • @NateS:没有人说这是个好主意。我们正在调查是否可行。显然是的。这不是拒绝我的理由。不要射击信使。如果您对我们为什么这样做有疑问,请否决 OP 的问题。我只展示如何。
  • 鼠标悬停在否决票上说,“这个答案没用”。对不起,但我觉得你的回答符合这个标准。您的回答表明可以更改最终值,而实际上并非如此。内联值不会更改。结果是完全不可预测的。我很欣赏分享这些深奥的知识,但永远不应该按照你的建议去做。
  • @NateS static final 编译时常量可以被 javac 内联。显然这里不是这样。这是尝试测试可能会改变的东西。虽然在运行时 final 字段可以内联,但实际上这对于单元测试来说应该不是问题。
【解决方案2】:

尝试调用文件的实例而不是类 File 的实例

例如

File file = ...;    
field.setChar(file,'/');

您也可以尝试http://code.google.com/p/jmockit/ 并模拟静态方法 FileSystem.getFileSystem()。 (不知道您是否可以模拟静态变量,通常不需要这些 hack -> 编写 oo 代码并使用“仅”模拟)

【讨论】:

    【解决方案3】:

    在构建文件时,只需在任何地方使用 /。我已经这样做了 13 年,从来没有遇到过问题。也没有什么可测试的。

    【讨论】:

      【解决方案4】:

      我知道这并不能直接回答您的问题,但 Apache Commons FileNameUtils 将进行跨平台文件名构造,并且可以节省您编写自己的类来执行此操作。

      【讨论】:

      • 我正在解构路径,而不是构建路径,但我可以做的是将所有内容转换为 unix 分隔符,运行我的算法,然后转换为系统分隔符。通过这种方式,我可以在不需要模拟 java.io.File 的情况下进行测试。
      • 我最终使用了这个解决方案,但下面的答案回答了我最初的问题,所以本着 stackoverflow 的精神,我接受了另一个答案。
      【解决方案5】:

      这里我将为“android.os.Build.VERSION.RELEASE”设置值,其中 VERSION 是类名,RELEASE 是最终的静态字符串值。

      如果基础字段是最终字段,则方法抛出 IllegalAccessException 所以我们需要使用 setAccessible(true) , 使用field.set()方法时需要添加NoSuchFieldException

      @RunWith(PowerMockRunner.class)
      @PrepareForTest({Build.VERSION.class})
      public class RuntimePermissionUtilsTest {
      @Test
      public void hasStoragePermissions() throws IllegalAccessException, NoSuchFieldException {
          Field field = Build.VERSION.class.getField("RELEASE");
          field.setAccessible(true);
          field.set(null,"Marshmallow");
       }
      }
      

      现在字符串 RELEASE 的值将返回“Marshmallow”。

      【讨论】:

        【解决方案6】:

        不要使用 File.separatorChar 来声明你的服务类,我们称之为 PathBuilder 什么的。这个类将有一个 concatPaths() 方法,它将连接两个参数(使用操作系统的分隔符)。美妙之处在于您正在编写这个类,因此您可以在对它进行单元测试时随意调整它。

        【讨论】:

          【解决方案7】:

          您可以获取 java.io.File 的源,并对其进行修改,使 separatorChar 和 separator 不是最终的,并添加一个 setSeparatorChar 方法来更新它们两者,然后将编译后的类包含在您的引导类路径中。

          【讨论】:

          • 您可能必须将该功能扩展到所有 java.io.FileSystem 实现。
          • 注意:不应部署使用此选项来覆盖 rt.jar 中的类的应用程序,因为这样做会违反 Java 2 运行时环境二进制代码许可。
          猜你喜欢
          • 2015-04-12
          • 2011-03-19
          • 2012-06-26
          • 1970-01-01
          • 1970-01-01
          • 2021-10-04
          • 1970-01-01
          相关资源
          最近更新 更多