使用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.separatorChar
是static
。 令人惊讶的是,有一种方法可以解决这个问题:只需通过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 ”。