Hot questions for Using Mockito in classloader

Question:

I have been running into issues trying to direct the Java ClassLoader to retrieve JSON files from the test/resources directory following deployment.

public class TestFileUtil {
    private static final ClassLoader classLoader = TestFileUtil.class.getClassLoader();

    public static Map<String, Object> getJsonFileAsMap(String fileLocation) {

        try {
            return new ObjectMapper().readValue(getTestFile(fileLocation), HashMap.class);
        } catch (IOException e) {
            throw new RuntimeException("Error converting JSON file to a Map", e);
        }
    }

    private static File getTestFile(String fileLocation) {

        return new File(classLoader.getResource(fileLocation).getFile());
    }
}

The utility has no problems during local testing with Mockito like so:

public class LocalTest {

    @Before
    public void setUp() {
        Mockito.when(mockDataRetrievalService.getAssetJsonById(Mockito.any())).thenReturn(TestFileUtil.getJsonFileAsMap("test.json"));
    }
}

However this line throws a FileNotFound exception when building in our deployed environments.

When using a relative directory path "../../test.json", I am seeing FileNotFound exceptions in both environments.

Local directory structure:

test
| java
| |- project
| |  |- LocalTest
| |- util
| |  |- TestFileUtil.class
| resources
| |- test.json

After deployment:

test
| com
| | project
| | | dao
| | | | LocalTest
| | other project
| | | | util
| | | | | TestFileUtil.class
| | | | | test.json

Is there any special behavior or a required directory structure associated with using the ClassLoader in automated builds?


Answer:

The problem most likely is this:

new File(classLoader.getResource(fileLocation).getFile());

The getFile() method of the URL class does not return a valid file name. It just returns the path portion of the URL, which is not guaranteed to be a valid filename. (The method name made sense when the URL class was introduced as part of Java 1.0, since nearly all URLs did in fact refer to physical files, either on the same machine or on a different machine.)

The argument to ClassLoader.getResource is not a filename. It's a relative URL, whose base is each location in the ClassLoader’s classpath. If you want to read a resource bundled with your application, do not try to convert the resource URL to a file. Read the URL as a URL instead:

public class TestFileUtil {
    private static final ClassLoader classLoader = TestFileUtil.class.getClassLoader();

    public static Map<String, Object> getJsonFileAsMap(String fileLocation) {

        try {
            return new ObjectMapper().readValue(getTestFile(fileLocation), HashMap.class);
        } catch (IOException e) {
            throw new RuntimeException("Error converting JSON file to a Map", e);
        }
    }

    private static URL getTestFile(String fileLocation) {

        return classLoader.getResource(fileLocation);
    }
}

If you want to read a file that is not part of your application, don’t use getResource at all. Just create a File instance.

Question:

I have two classes to try to figure out how whenNew works.

public class RockService {
    public RockData serv() {
        RockData rockData = new RockData();
        rockData.setName("RockService");
        rockData.setContent("content from rock service");
        return rockData;
    } }

And

public class RockData {
    String name;
    long id;
    String content;
    // get set method ignored
}

With test code

@RunWith(PowerMockRunner.class)
@PrepareForTest(RockService.class)
public class MockNewInstanceCreation {

    @Test
    public void mockCreationTest() throws Exception {
        RockData rockData = mock(RockData.class);

        when(rockData.getName()).thenReturn("this is mock");

        whenNew(RockData.class).withNoArguments().thenReturn(rockData);

        RockService rockService = new RockService();

        RockData servData = rockService.serv();
        System.out.println(servData.getName());
        System.out.println(servData.getContent());
    }
}

So at runtime, if not mock, the output (RockData's getName()) would be "RockService". But with mock, it returns "this is mock". The code works but still I didn't know how exactly Powermock/Mockito did this.

I debugged the code. What confused me is after RockData rockData = new RockData(); executed, what actually created is exactly the instance that created by RockData rockData = mock(RockData.class);. Which means new RockData() doesn't create an new instance at all. It just returned an instance that already created. And when debugging, it jumped to MockGateway.newInstanceCall.

So how does Powermockito intercept new instance?


Answer:

PowerMockRunner runs tests using special class loader - org.powermock.core.classloader.MockClassLoader.

Instead of loading real class it loads a new one with the same signature. It means that the real constructor won't be invoked.

So the object that is returned by new operator is not a Mock. It is a instance of a different class that could be assigned to the real one and it returns mocked values.

See the code below:

public class RockService {

    ClassLoader classLoader = this.getClass().getClassLoader();

    System.out.println("Real construct");
    //Different class loader
    System.out.println(classLoader);
    //The same class
    System.out.println(this.getClass());
}

public RockData serv() {

    RockData rockData = new RockData();

    Class<? extends RockData> clazz = rockData.getClass();
    //This is a different class 
    System.out.println("Mocked class: " + clazz.getCanonicalName());
    //And different classloader
    ClassLoader classLoader = clazz.getClassLoader();
    System.out.println(classLoader);
    //Mocked class instance could be assigned to real one
    System.out.println(RockData.class.isAssignableFrom(clazz));

    //it's instance of both RockData.class and mocked class
    System.out.println(clazz.isInstance(rockData));
    System.out.println(RockData.class.isInstance(rockData));

    rockData.setName("RockService");
    rockData.setContent("content from rock service");
    return rockData;
}