views:

353

answers:

6

Specifically, I'm trying to create a unit test for a method which requires uses File.separatorChar to build paths on windows and unix. The code must run on both platforms, and yet I get errors with JUnit when I attempt to change this static final field.

Anyone have any idea what's going on?

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

When I do this, I get

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

Thoughts?

A: 

Instead of using File.separatorChar declare your service Class, let's call it PathBuilder or something. This class will have a concatPaths() method which will concatenate the two parameters (using the OS's separator char). The beauty is that you are writing this class so you can tweak it anyway you want when you unit test it.

Itay
+1  A: 

Try invoking on an instance of file not on an instance of class File

E.g.

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

You could also try http://code.google.com/p/jmockit/ and mock the static method FileSystem.getFileSystem(). (don't know if you can mock static variables, normally those hacks shouldn't be necessary -> write oo code and use 'only' mockito)

Karussell
+1  A: 

I realise this doesn't answer your question directly, but Apache Commons FileNameUtils will do cross-platform filename construction, and may save you writing your own class to do this.

Brian Agnew
I'm deconstructing a path, rather than constructing, but what I could do is convert everything to unix separators, run my algorithm, and then convert to system separators. In this manner, I can test without needing to mock java.io.File.
Stefan Kendall
I wound up using this solution, but the answer below answered my initial question, so in the spirit of stackoverflow, I accepted the other answer.
Stefan Kendall
A: 

You can take the source for java.io.File, and modify it so that separatorChar and separator are not final, and add a setSeparatorChar method that updates the two of them, then include the compiled class in your bootclasspath.

Stephen Denne
You may have to extend that ability to all java.io.FileSystem implementations.
Stephen Denne
Note: Applications that use this option for the purpose of overriding a class in rt.jar should not be deployed as doing so would contravene the Java 2 Runtime Environment binary code license.
Stephen Denne
+8  A: 

From the documentation for Field.set:

If the underlying field is final, the method throws an IllegalAccessException unless setAccessible(true) has succeeded for this field and this field is non-static.

So at first it seems that you are out of luck, since File.separatorChar is static. Surprisingly, there is a way to get around this: simply make the static field no longer final through reflection.

I adapted this solution 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);
}

I've tested it and it works:

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

Do exercise extreme caution with this technique. Devastating consequences aside, the following actually works:

setFinalStatic(Boolean.class.getField("FALSE"), true);
System.out.format("Everything is %s", false); // "Everything is true"
polygenelubricants
#define FALSE 1is back, and it's ready to kick some ass all over again.
Stefan Kendall
I can finally unit test against quantum fluctuation.
Stefan Kendall
Holy crap! The introduction of setAccessible is a major screw up.
Software Monkey
This is NOT a good idea. While it may work for your test, the final value can be inlined and when you change the value of the field, it will not change the value inlined in method bodies.
NateS
@NateS: no one is saying that this is a good idea. We were investigating if something is doable. Apparently it is. That's not a reason to downvote me. Don't shoot the messenger. If you have a problem with WHY we're doing this, downvote OP's question. I'm only showing HOW.
polygenelubricants
Mouse over on down vote says, "This answer is not useful". Sorry, but I feel your answer meets this criteria. Your answer makes it appear that it is possible to change final values when in fact it is not. The inlined values will not be changed. The results are totally unpredicable. I appreciate the sharing of such esoteric knowledge, but doing what you suggest should never, ever be done.
NateS
@NateS `static final` *compile-time constants* can be inlined by javac. Obviously that isn't the case here. This is an attempt to test something that might change. Whilst at runtime `final` fields can be inlined, in practice that should not be a problem for unit tests.
Tom Hawtin - tackline
@Tom Hastin - tackline, the static final constant File.separator can be inlined by the JIT. I don't see how undefined behavior "should not be a problem for unit tests" when it may or may not be testing what you think it is.
NateS
JLS 17.5.3 details when it is OK to modify final fields and the associated problems:http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5.3
NateS
+1  A: 

Just use / everywhere when constructing Files. I've been doing that for 13 years and never had a problem. Nothing to test either.

EJP