Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can mock construction of 'new File()' with Mockito.mockConstruction #2342

Closed
popshi opened this issue Jun 24, 2021 · 4 comments
Closed

Can mock construction of 'new File()' with Mockito.mockConstruction #2342

popshi opened this issue Jun 24, 2021 · 4 comments

Comments

@popshi
Copy link

popshi commented Jun 24, 2021

Hello,

I can't use Mockito.mockConstruction to mock the construction of 'File'.
https://stackoverflow.com/questions/68097251/can-the-mockito3-mockconstruction-make-stub-on-new-file

It's better to have a way to return mock object or real object from the 'mockConstruction'.

         try (MockedConstruction<File> ignored = Mockito.mockConstruction(File.class,
                (context) -> {
                    if( context.arguments().get(0) == "1" } {
                        File mock = mock(File.class);
                        return mock;
                    } else {
                        return (File)context.constructor().newInstance(context.arguments().toArray());
                    }

@lesiak
Copy link

lesiak commented Jun 24, 2021

I took a look at the issue, and it seems to loop at MethodGraph.doAnalyze

Consider the following test:

public class ConstructionTest {

    static class Foo /* implements Serializable, Comparable<Foo> */ {
        String method() {
            return "foo";
        }

        public int compareTo(Foo o) {
            return 0;
        }
    }


    @Test
    void testFoo() {
        try (MockedConstruction mocked = mockConstruction(Foo.class)) {
            Foo foo = new Foo();
            when(foo.method()).thenReturn("bar");
            assertEquals("bar", foo.method());
            verify(foo).method();
        }
    }

    @Test
    void testFile() {
        try (MockedConstruction mocked = mockConstruction(File.class)) {
            File f = new File("test");
            when(f.exists()).thenReturn(true);
            assertEquals(true, f.exists());
            verify(f).exists();
        }
    }

}

EXPECTED BEHAVIOUR: both test pass
OBSERVED BEHAVIOUR:

  • testFoo passed
  • testFile fails with StackOverflowException
java.lang.StackOverflowError
	at java.base/java.lang.String.indexOf(String.java:1765)
	at java.base/java.lang.String.indexOf(String.java:1731)
	at java.base/java.net.URLStreamHandler.parseURL(URLStreamHandler.java:288)
	at java.base/sun.net.www.protocol.file.Handler.parseURL(Handler.java:67)
	at java.base/java.net.URL.<init>(URL.java:701)
	at java.base/java.net.URL.<init>(URL.java:568)
	at java.base/jdk.internal.loader.URLClassPath$FileLoader.getResource(URLClassPath.java:1216)
	at java.base/jdk.internal.loader.URLClassPath.getResource(URLClassPath.java:317)
	at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:720)
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:646)
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:604)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
	at net.bytebuddy.description.type.TypeDescription$Generic$OfParameterizedType.getInterfaces(TypeDescription.java:4835)
	at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$Default.doAnalyze(MethodGraph.java:631)
	at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$Default.analyze(MethodGraph.java:596)
	at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$Default.doAnalyze(MethodGraph.java:632)
	at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$Default.compile(MethodGraph.java:567)
	at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$AbstractBase.compile(MethodGraph.java:465)
	at org.mockito.internal.creation.bytebuddy.MockMethodAdvice.isOverridden(MockMethodAdvice.java:209)
	at java.base/java.io.File.exists(File.java:816)
	at java.base/jdk.internal.loader.URLClassPath$FileLoader.getResource(URLClassPath.java:1239)
	at java.base/jdk.internal.loader.URLClassPath.getResource(URLClassPath.java:317)
	at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:720)
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:646)
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:604)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
	at net.bytebuddy.description.type.TypeDescription$Generic$OfParameterizedType.getInterfaces(TypeDescription.java:4835)
	at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$Default.doAnalyze(MethodGraph.java:631)
	at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$Default.analyze(MethodGraph.java:596)
	at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$Default.doAnalyze(MethodGraph.java:632)
	at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$Default.compile(MethodGraph.java:567)
	at net.bytebuddy.dynamic.scaffold.MethodGraph$Compiler$AbstractBase.compile(MethodGraph.java:465)
	at org.mockito.internal.creation.bytebuddy.MockMethodAdvice.isOverridden(MockMethodAdvice.java:209)
	at java.base/java.io.File.exists(File.java:816)
	at java.base/jdk.internal.loader.URLClassPath$FileLoader.getResource(URLClassPath.java:1239)
	at java.base/jdk.internal.loader.URLClassPath.getResource(URLClassPath.java:317)
	at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:720)
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:646)
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:604)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
	at net.bytebuddy.description.type.TypeDescription$Generic$OfParameterizedType.getInterfaces(TypeDescription.java:4835)
   //  ... Continues ...

After that, I changed the definition of Foo class to match the structure of File:

 static class Foo implements Serializable, Comparable<Foo> {
        String method() {
            return "foo";
        }

        public int compareTo(Foo o) {
            return 0;
        }
}

Strangely, now BOTH tests pass (assuming testFoo is launched before testFile).

I debugged each test separately and for File, in MethodGraph.doAnalyze it visits:

  • TypeDefinition$ForLoadedType@3842 "class java.io.File"
  • TypeDescription$Generic$OfNonGenericType$ForLoadedType "class java.lang.Object"
  • TypeDescription$Generic$OfNonGenericType$ForLoadedType "interface java.io.Serializable"
  • TypeDescription$Generic$OfParameterizedType$ForLoadedType (StackOverflow at toString, but parametrizedType is "java.lang.Comparable<java.io.File>")
  • TypeDefinition$ForLoadedType@3876 "class java.io.File"
  • // NOTE Same type, but different instance. Loops from here

For Foo, in in MethodGraph.doAnalyze there is no loop. It visits:

  • TypeDefinition$ForLoadedType@3833 "class dev.construction.ConstructionTest$Foo"
  • TypeDescription$Generic$OfNonGenericType$ForLoadedType "class java.lang.Object"
  • TypeDescription$Generic$OfNonGenericType$ForLoadedType "interface java.io.Serializable"
  • TypeDescription$Generic$OfParameterizedType$ForLoadedType "java.lang.Comparable<dev.construction.ConstructionTest$Foo>"

My setup:
openjdk 15.0.1 2020-10-20
OpenJDK Runtime Environment (build 15.0.1+9-18)
OpenJDK 64-Bit Server VM (build 15.0.1+9-18, mixed mode, sharing)

Dependencies:

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
    testImplementation 'org.mockito:mockito-inline:3.11.2'
    testImplementation 'org.mockito:mockito-junit-jupiter:3.11.2'
}

@MrOshima
Copy link

MrOshima commented Jul 3, 2021

If the File or URL classes are changed by mockConstruction() the MockMethodAdvice.isOverriden() is called.
Inside this method new instance of TypeDescription.ForLoadedType is created:

if (methodGraph == null) {
methodGraph = compiler.compile(new TypeDescription.ForLoadedType(instance.getClass()));
graphs.put(instance.getClass(), new SoftReference<>(methodGraph));
}

To load the ForLoadedType class the URLClassPath.getResource() method is called. The body of this method contains File and URL objects.
If the File is mocked the file.exsits() will cause the recurrence. If URL is mocked url.getFile() will do this.

@popshi
Copy link
Author

popshi commented Sep 7, 2021

Do you think mockito should resolve the issue or do we have workaround to mock 'new File()';

@TimvdLippe
Copy link
Contributor

Unfortunately, File is one of the classes that Mockito relies on internally for its behavior. Stubbing File will therefore lead to undefined behavior. Additionally, it is advised not to mock classes you don't own: https://github.com/mockito/mockito/wiki/How-to-write-good-tests#dont-mock-a-type-you-dont-own We are working on improving the user experience by working on a DoNotMock feature to avoid mocking classes/methods that are known to crash Mockito internals (#1833). Therefore, I am closing this as "Infeasible". Apologies for the uninformative exception that is thrown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants