使用reflection更改静态最终File.separatorChar进行unit testing?

具体来说,我正在尝试为一个方法创建一个unit testing,该方法需要使用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 

思考?

Field.set的文档:

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

所以起初看起来你运气不好,因为File.separatorCharstatic 。 令人惊讶的是,有一种方法可以解决这个问题:只需通过reflection使static场不再是final

我从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" 

重要更新 :上述解决方案并非在所有情况下都有效。 如果该字段在重置之前可以访问并通过Reflection读取,则抛出IllegalAccessException 。 它失败是因为Reflection 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 

尝试调用不在类File实例上的文件实例

例如

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

您也可以尝试http://code.google.com/p/jmockit/并模拟静态方法FileSystem.getFileSystem()。 (不知道你是否可以模拟静态变量,通常那些黑客不应该是必要的 – >写oo代码并使用’only’tileito)

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

我意识到这不能直接回答你的问题,但是Apache Commons FileNameUtils会做跨平台的文件名构建,并且可以节省你编写自己的类来做到这一点。

而不是使用File.separatorChar声明您的服务类,让我们称之为PathBuilder或其他东西。 这个类将有一个concatPaths()方法,它将连接这两个参数(使用操作系统的分隔符char)。 美妙之处在于您正在编写本课程,因此您可以在进行unit testing时随意调整它。

你可以获取java.io.File的源代码,并修改它以使separatorChar和separator不是final,并添加一个setSeparatorChar方法来更新它们中的两个,然后在bootclasspath中包含已编译的类。

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

如果底层字段是final,则该方法抛出 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"); } } 

现在String RELEASE的值将返回“ Marshmallow ”。