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

Test fails after enabling feature for mocking final classes and methods #1005

Open
geralt-encore opened this issue Mar 24, 2017 · 18 comments
Open
Assignees

Comments

@geralt-encore
Copy link

I am mocking GoogleSignInAccount with the latest Mockito version:

GoogleSignInAccount mockGoogleAccount = mock(GoogleSignInAccount.class);
    when(mockGoogleAccount.getId()).thenReturn("id");
    when(mockGoogleAccount.getEmail()).thenReturn("email@email.com");
    when(mockGoogleAccount.getFamilyName()).thenReturn("family name");
    when(mockGoogleAccount.getGivenName()).thenReturn("given name");

After enabling mocking of final classes/methods test fails with this error (which is not really helpful):

org.mockito.exceptions.misusing.MissingMethodInvocationException: 
when() requires an argument which has to be 'a method call on a mock'.
For example:
    when(mock.getArticles()).thenReturn(articles);

Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
   Those methods *cannot* be stubbed/verified.
   Mocking methods declared on non-public parent classes is not supported.
2. inside when() you don't call method on mock but on some other object.

	at com.app.profile.data.ProfileRepositoryTest.shouldFailRegisterIfEmailNotRegisteredAndUnknownGenderGoogleAccount(ProfileRepositoryTest.java:870)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:117)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:262)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:84)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

Let me know if you need more details from me.

@TimvdLippe
Copy link
Contributor

@geralt-encore Could you build a gist so that we can easily test it? I am not familiar with the Android ecosystem, would like to have a quick install to debug directly.

@geralt-encore
Copy link
Author

Here is a sample project for reproduction: https://github.com/geralt-encore/MockitoBug

@TimvdLippe
Copy link
Contributor

I have assigned @raphw, as he is probably the most knowledgeable about this bug. However, he is quite busy IRL so it might take some time before he is able to respond. Thanks for your understanding!

@geralt-encore
Copy link
Author

Sure! Let me know if you need more info from me.

@raphw
Copy link
Member

raphw commented Mar 28, 2017

I do not currently have an Android SDK installed but I assume it has something to do with your use of Android? The inline mock maker requires running on a "regular" JVM and cannot be used on Android or on an emulator.

@tmurakami
Copy link
Contributor

@raphw, I am interested in this issue and I investigated this with the above reproducible project.
This problem occurs on the "regular" JVM such as OpenJDK, not Android VM.

I suppose this is a bug of byte-buddy.

Running the code above causes ArrayIndexOutOfBoundsException.
(However, since Throwable objects are ignored here, this exception will not appear.)

Here is the stack trace elements.

0 = {StackTraceElement@1934} "net.bytebuddy.asm.Advice$StackMapFrameHandler$Default.translateFrame(Advice.java:1194)"
1 = {StackTraceElement@1935} "net.bytebuddy.asm.Advice$StackMapFrameHandler$Default.translateFrame(Advice.java:1141)"
2 = {StackTraceElement@1936} "net.bytebuddy.asm.Advice$AdviceVisitor.visitFrame(Advice.java:6590)"
3 = {StackTraceElement@1937} "net.bytebuddy.jar.asm.ClassReader.a(Unknown Source)"
4 = {StackTraceElement@1938} "net.bytebuddy.jar.asm.ClassReader.b(Unknown Source)"
5 = {StackTraceElement@1939} "net.bytebuddy.jar.asm.ClassReader.accept(Unknown Source)"
6 = {StackTraceElement@1940} "net.bytebuddy.jar.asm.ClassReader.accept(Unknown Source)"
7 = {StackTraceElement@1941} "net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForInlining.create(TypeWriter.java:2910)"
8 = {StackTraceElement@1942} "net.bytebuddy.dynamic.scaffold.TypeWriter$Default.make(TypeWriter.java:1628)"
9 = {StackTraceElement@1943} "net.bytebuddy.dynamic.scaffold.inline.RedefinitionDynamicTypeBuilder.make(RedefinitionDynamicTypeBuilder.java:171)"
10 = {StackTraceElement@1944} "net.bytebuddy.dynamic.scaffold.inline.AbstractInliningDynamicTypeBuilder.make(AbstractInliningDynamicTypeBuilder.java:92)"
11 = {StackTraceElement@1945} "net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase.make(DynamicType.java:2560)"
12 = {StackTraceElement@1946} "org.mockito.internal.creation.bytebuddy.InlineBytecodeGenerator.transform(InlineBytecodeGenerator.java:156)"
[snip]

The exception has occurred here.

I set a breakpoint at the place where the exception occurred.
Here is a list of variables.

this = {Advice$StackMapFrameHandler$Default@1896} 
methodVisitor = {LineNumberPrependingMethodVisitor@1900} 
translationMode = {Advice$StackMapFrameHandler$Default$TranslationMode$1@1901} "COPY"
methodDescription = {MethodDescription$Latent@1902} "public android.accounts.Account com.google.android.gms.auth.api.signin.GoogleSignInAccount.getAccount()"
additionalTypes = {TypeList$Explicit@1903}  size = 1
type = 0
localVariableLength = 0
localVariable = {Object[1]@1904} 
stackSize = 1
stack = {Object[4]@1905} 
translated = {Object[1]@1954} 
index = 1
typeDescription = {TypeDescription$ForLoadedType@1917} "interface java.util.concurrent.Callable"
translated.length = 1
instrumentedType = {InstrumentedType$Default@1906} "class com.google.android.gms.auth.api.signin.GoogleSignInAccount"
instrumentedMethod = {MethodDescription$Latent@1902} "public android.accounts.Account com.google.android.gms.auth.api.signin.GoogleSignInAccount.getAccount()"

Since the length of the array translated is 1 and index is 1, the exception is occurred.

methodDescription is forGoogleSignInAccount#getAccount, so I got javap of this method.

  public android.accounts.Account getAccount();
    descriptor: ()Landroid/accounts/Account;
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=1, args_size=1
         0: aload_0
         1: getfield      #53                 // Field zzaka:Ljava/lang/String;
         4: ifnonnull     11
         7: aconst_null
         8: goto          24
        11: new           #13                 // class android/accounts/Account
        14: dup
        15: aload_0
        16: getfield      #53                 // Field zzaka:Ljava/lang/String;
        19: ldc           #1                  // String com.google
        21: invokespecial #60                 // Method android/accounts/Account."<init>":(Ljava/lang/String;Ljava/lang/String;)V
        24: areturn
      StackMapTable: number_of_entries = 2
        frame_type = 11 /* same */
        frame_type = 255 /* full_frame */
          offset_delta = 12
          locals = []
          stack = [ class android/accounts/Account ]
    RuntimeInvisibleAnnotations:
      0: #231()

The stack map table has two entries.
First is same, second is full_frame.
(The exception occurs while processing the entry of type full_frame.)

Moreover I ran the following code.

package com.geraltencore.mockitobug;

import com.google.android.gms.auth.api.signin.GoogleSignInAccount;

import net.bytebuddy.jar.asm.ClassReader;
import net.bytebuddy.jar.asm.ClassVisitor;
import net.bytebuddy.jar.asm.MethodVisitor;
import net.bytebuddy.jar.asm.Opcodes;

import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Arrays;

public class MyTest {

    private static final String[] TYPE_NAMES = {
            "F_NEW",
            "F_FULL",
            "F_APPEND",
            "F_CHOP",
            "F_SAME",
            "F_SAME1",
    };

    @Test
    public void test() throws Exception {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        InputStream in = getClass().getResourceAsStream('/' + GoogleSignInAccount.class.getName().replace('.', '/') + ".class");
        try {
            byte[] buffer = new byte[8192];
            for (int l; (l = in.read(buffer)) != -1; ) {
                out.write(buffer, 0, l);
            }
        } finally {
            in.close();
        }
        new ClassReader(out.toByteArray()).accept(new ClassVisitor(Opcodes.ASM5) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                return name.equals("getAccount") ? new MethodVisitor(api, mv) {

                    private int n = 0;

                    @Override
                    public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
                        super.visitFrame(type, nLocal, local, nStack, stack);
                        System.out.println("#" + n++);
                        System.out.println("  type=" + TYPE_NAMES[type + 1]);
                        System.out.println("  nLocal=" + nLocal);
                        System.out.println("  local=" + Arrays.toString(local));
                        System.out.println("  nStack=" + nStack);
                        System.out.println("  stack=" + Arrays.toString(stack));
                    }

                } : mv;
            }
        }, 0);
    }

}

Here is the result.

#0
  type=F_SAME
  nLocal=0
  local=[null]
  nStack=0
  stack=[null, null, null, null]
#1
  type=F_FULL
  nLocal=0
  local=[null]
  nStack=1
  stack=[android/accounts/Account, null, null, null]

nLocal is 0, but the length of the array local is 1.

Let me know if you need more information.

@geralt-encore
Copy link
Author

geralt-encore commented Mar 29, 2017

As @tmurakami pointed I am not running this test on Android - it occurs on regular JVM.
I am also experiencing the same issue when trying to mock FirebaseRemoteConfig. I can update the sample project with a test for this class if will be helpful.

@ChristianSchwarz
Copy link
Contributor

ChristianSchwarz commented Mar 29, 2017

I observed the same in #939 and possibly it relates to #978 also.

Edit: Can we have a Android label?

@tmurakami
Copy link
Contributor

@ChristianSchwarz, I think this is not Android specific problem.
It seems to occur while processing the StackMapTable attribute with a frame of type full_frame.
(The StackMapTable attribute is described in the Java Virtual Machine Specification.)

The issue #939 and #978 are that InlineByteBuddyMockMaker cannot be initialized.
This issue is that Mockito cannot mock a specific class correctly with inline mock making.
I think that this is different from those issues.

@raphw
Copy link
Member

raphw commented Mar 30, 2017

This is an interesting error. The problem is the full frame that does not specify any local variables. This is incorrect for the method in question which is non-static and should therefore always specify a 'this' value. The compiler that created this method apparently voided this variable.

As a consequence, Byte Buddy cannot longer instrument the method as it requires access to the "this" reference even after the method return which has now become impossible.

That the error is suppressed is unfortunate and we need to improve this. The error itself can however not be fixed as the stack map frames are already corrupted.

It would be interesting to know how these frames got in there. Creating the same class and compiling it with javac yields correct frames.

raphw added a commit that referenced this issue Mar 30, 2017
…to mock maker that attempts instrumentation. Addresses #1005
@tmurakami
Copy link
Contributor

@raphw, thank you for your reply.

GoogleSignInAccount provided by Google seems to be obfuscated by ProGuard.
StackMapTable might have been broken due to obfuscation.

@geralt-encore, I tried using -noverify option on your reproducible project and it works fine.
Try the following snippet.

android {
    testOptions {
        unitTests.all {
            jvmArgs '-noverify'
        }
    }
}

@geralt-encore
Copy link
Author

@tmurakami I just tried it and it didn't work. Did I miss something? Looks pretty straightforward.

@tmurakami
Copy link
Contributor

Add the -noverify option into your app/build.gradle.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.geraltencore.mockitobug"
        minSdkVersion 16
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    testOptions {
        unitTests.all {
            jvmArgs '-noverify'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.0'
    compile 'com.google.android.gms:play-services-auth:10.2.1'
    testCompile 'org.mockito:mockito-core:2.7.19'
    testCompile 'junit:junit:4.12'
}

Then, run ./gradlew clean test.

Here is my Java environment:

openjdk version "1.8.0_121"
OpenJDK Runtime Environment (build 1.8.0_121-8u121-b13-0ubuntu1.16.10.2-b13)
OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)

If you run test on Android Studio, set -noverify as VM options in the Run/Debug Configurations dialog.

@geralt-encore
Copy link
Author

I see, ran it from Android Studio and forgot that it won't use options specified in Gradle. Are there any downsides of using it?

@tmurakami
Copy link
Contributor

Are there any downsides of using it?

-noverify disables bytecode verification, so should not be used if possible.
However I cannot think of any other way.

TimvdLippe pushed a commit that referenced this issue Apr 18, 2017
…to mock maker that attempts instrumentation. Addresses #1005 (#1012)
@ChristianSchwarz
Copy link
Contributor

The Android Framework Team is going to provide better mock support with dexmaker-mockito-inline see PR: linkedin/dexmaker#68

@tmurakami
Copy link
Contributor

@ChristianSchwarz, thank you for your information.
However, that PR will not solve this issue.

Generally, an Android project has two kinds of tests.

  1. Local Unit Tests (on JVM)
  2. Instrumented Unit Tests (on Android VM)

That PR seems to provide inline mocking feature on Android VM using slicer library.

On the other hand, this problem occurs while running local unit tests, so unfortunately that PR will not help this issue.

@vikasgill
Copy link

I'm trying to run the Instrument Test and getting the following error:

org.mockito.exceptions.base.MockitoException:
Cannot mock/spy class android.hardware.radio.ProgramList
Mockito cannot mock/spy because :

  • final or anonymous class

The build.gradle file includes the following
`
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.haleytek.radio.service"
minSdkVersion 30
targetSdkVersion 30
versionCode 99
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}

dependencies {
implementation fileTree(include: ['.jar', '.aar'], dir: '../libs/gradle')

implementation project(':commonutils')
implementation project(':interface')
implementation project(':tunersupportlibrary')

implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.jakewharton.timber:timber:4.7.1'

// Room database dependency with rxjava support
implementation "androidx.room:room-runtime:2.3.0"
annotationProcessor "androidx.room:room-compiler:2.3.0"
implementation "androidx.room:room-rxjava2:2.3.0"
implementation "androidx.preference:preference:1.1.1"

// RxJava dependency
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.9'

testImplementation 'junit:junit:4.13.1'
testImplementation 'org.robolectric:robolectric:4.7.3'
testImplementation 'androidx.test:core:1.4.0'
testImplementation 'org.hamcrest:hamcrest:2.2'

androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
// For being able to mock final classes in instrumentation tests:



//testImplementation 'org.mockito:mockito-core:2.24.0'
//androidTestImplementation 'org.mockito:mockito-android:2.24.0'

api 'org.mockito:mockito-core:2.28.2', { exclude group: 'net.bytebuddy' }
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:2.28.0"

}
`

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

6 participants