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

Mocking static methods not working anymore since version 4.2.0 #2530

Closed
Hakky54 opened this issue Jan 1, 2022 · 9 comments
Closed

Mocking static methods not working anymore since version 4.2.0 #2530

Hakky54 opened this issue Jan 1, 2022 · 9 comments

Comments

@Hakky54
Copy link

Hakky54 commented Jan 1, 2022

Hello, recently I tried to upgrade to the latest Mockito version from 4.1.0 to 4.2.0 which resulted in a failing build. I noticed that the unit tests which used mockstatic was not passing anymore. I can share a simple code snippet which worked and does not work anymore with the latest version:

FooUtils

public final class FooUtils {

    private FooUtils() {}

    public static String getName() {
        return "Foo";
    }

}

Unit Test

import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;

import static org.assertj.core.api.Assertions.assertThat;

class FooTest {

    @Test
    void getName() {
        try (MockedStatic<FooUtils> mockedFooUtils = Mockito.mockStatic(FooUtils.class, InvocationOnMock::getMock)) {
            mockedFooUtils.when(FooUtils::getName).thenReturn("bar");
            String name = FooUtils.getName();
            assertThat(name).isEqualTo("bar");
        }
    }

}

The test fails and I am getting the following error:

org.mockito.exceptions.misusing.WrongTypeOfReturnValue: 
Default answer returned a result with the wrong type:
Class cannot be returned by getName()
getName() should return String

The default answer of FooUtils.class that was configured on the mock is probably incorrectly implemented.

See also this project where I use mockstatic: https://github.com/Hakky54/sslcontext-kickstart
The master branch is with mockito 4.1.0 which passes however this pull request where the latest version is being used fails: Hakky54/sslcontext-kickstart#135

I am using Java 8, 11 and 17 and it fails on all of them. I am using mockito-junit-jupiter. Any idea what could be the cause? Do you think I am using the mockstatic wrong or do I need additional configuration for the latest version?

Looking forward to hear from you guys soon.

Hakan,

@temp-droid
Copy link
Contributor

Hey @TimvdLippe , I think it is related to our recent changes here: #2506.

Not sure what is happening, but it is related to WrongTypeOfReturnValue kind of exception:

org.mockito.exceptions.misusing.WrongTypeOfReturnValue: 
Default answer returned a result with the wrong type:
Class cannot be returned by getRootCaFromAuthorityInfoAccessExtensionIfPresent()
getRootCaFromAuthorityInfoAccessExtensionIfPresent() should return List

@naninuneno
Copy link

mockStatic()'s 2nd argument: the default answer when invoking static methods

try either dropping this argument:

try (MockedStatic<FooUtils> mockedFooUtils = Mockito.mockStatic(FooUtils.class)) {
    mockedFooUtils.when(FooUtils::getName).thenReturn("bar");
    String name = FooUtils.getName();
    assertThat(name).isEqualTo("bar");
}

or specify the default as the return type you need:

try (MockedStatic<FooUtils> ignored = Mockito.mockStatic(FooUtils.class, invocation -> "bar")) {
    String name = FooUtils.getName();
    assertThat(name).isEqualTo("bar");
}

@Hakky54
Copy link
Author

Hakky54 commented Jan 1, 2022

mockStatic()'s 2nd argument: the default answer when invoking static methods

Dropping the second parameter fixes the issue of the example with FooUtils, but in some cases I need to partially mock and therefor need the second parameter. I have couple of uses cases here:

I have copied one of the unit tests of the project below:

@Test
void getRootCaIfPossibleReturnsJdkTrustedCaCertificateWhenNoAuthorityInfoAccessExtensionIsPresent() {
    List<Certificate> certificates = CertificateUtils.getCertificate("https://www.reddit.com/")
            .get("https://www.reddit.com/");

    try (MockedStatic<CertificateUtils> certificateUtilsMockedStatic = mockStatic(CertificateUtils.class, invocation -> {
        Method method = invocation.getMethod();
        if ("getRootCaFromAuthorityInfoAccessExtensionIfPresent".equals(method.getName())) {
            return invocation.getMock();
        } else {
            return invocation.callRealMethod();
        }
    })) {
        certificateUtilsMockedStatic.when(() -> CertificateUtils.getRootCaFromAuthorityInfoAccessExtensionIfPresent(any(X509Certificate.class)))
                .thenReturn(Collections.emptyList());

        X509Certificate certificate = (X509Certificate) certificates.get(certificates.size() - 1);
        List<X509Certificate> rootCaCertificate = CertificateUtils.getRootCaIfPossible(certificate);
        assertThat(rootCaCertificate).isNotEmpty();

        certificateUtilsMockedStatic.verify(() -> CertificateUtils.getRootCaFromJdkTrustedCertificates(certificate), times(1));
    }
}

The error message for the above test is:

org.mockito.exceptions.misusing.WrongTypeOfReturnValue: 
Default answer returned a result with the wrong type:
Class cannot be returned by getRootCaFromAuthorityInfoAccessExtensionIfPresent()
getRootCaFromAuthorityInfoAccessExtensionIfPresent() should return List

The default answer of CertificateUtils.class that was configured on the mock is probably incorrectly implemented.

Any idea why the above one could be failing?

@Hakky54
Copy link
Author

Hakky54 commented Jan 1, 2022

I tried to add a cast to the returned object for the mocked one and then it passes. So I added:
return (List) invocation.getMock();

So the following test passes when having a explicite cast:

@Test
void getRootCaIfPossibleReturnsJdkTrustedCaCertificateWhenNoAuthorityInfoAccessExtensionIsPresent() {
    List<Certificate> certificates = CertificateUtils.getCertificate("https://www.reddit.com/")
            .get("https://www.reddit.com/");

    try (MockedStatic<CertificateUtils> certificateUtilsMockedStatic = mockStatic(CertificateUtils.class, invocation -> {
        Method method = invocation.getMethod();
        if ("getRootCaFromAuthorityInfoAccessExtensionIfPresent".equals(method.getName())) {
            return (List) invocation.getMock();
        } else {
            return invocation.callRealMethod();
        }
    })) {
        certificateUtilsMockedStatic.when(() -> CertificateUtils.getRootCaFromAuthorityInfoAccessExtensionIfPresent(any(X509Certificate.class)))
                .thenReturn(Collections.emptyList());

        X509Certificate certificate = (X509Certificate) certificates.get(certificates.size() - 1);
        List<X509Certificate> rootCaCertificate = CertificateUtils.getRootCaIfPossible(certificate);
        assertThat(rootCaCertificate).isNotEmpty();

        certificateUtilsMockedStatic.verify(() -> CertificateUtils.getRootCaFromJdkTrustedCertificates(certificate), times(1));
    }
}

https://github.com/Hakky54/sslcontext-kickstart/blob/19e8d74f0fb8d661693393354d4a1bba3471002f/sslcontext-kickstart/src/test/java/nl/altindag/ssl/util/CertificateUtilsShould.java#L212

@temp-droid
Copy link
Contributor

           if ("getRootCaFromAuthorityInfoAccessExtensionIfPresent".equals(method.getName())) {
                return invocation.getMock();
            } else {
                return invocation.callRealMethod();
            }

invocation.getMock() returns a Class, could this be the class mentionned in Class cannot be returned by getRootCaFromAuthorityInfoAccessExtensionIfPresent()?

@temp-droid
Copy link
Contributor

@Hakky54 , could you try to replace your code with:

if ("getRootCaFromAuthorityInfoAccessExtensionIfPresent".equals(method.getName())) {
    return Collections.emptyList();
}

The test passes with that change for me.

@Hakky54
Copy link
Author

Hakky54 commented Jan 1, 2022

Yes @temp-droid I can confirm that it is working like that.

I have rewritten the test like this and it passes:

@Test
void getRootCaIfPossibleReturnsJdkTrustedCaCertificateWhenNoAuthorityInfoAccessExtensionIsPresent() {
    List<Certificate> certificates = CertificateUtils.getCertificate("https://www.reddit.com/")
            .get("https://www.reddit.com/");

    try (MockedStatic<CertificateUtils> certificateUtilsMockedStatic = mockStatic(CertificateUtils.class, invocation -> {
        Method method = invocation.getMethod();
        if ("getRootCaFromAuthorityInfoAccessExtensionIfPresent".equals(method.getName())) {
            return Collections.emptyList();
        } else {
            return invocation.callRealMethod();
        }
    })) {
        X509Certificate certificate = (X509Certificate) certificates.get(certificates.size() - 1);
        List<X509Certificate> rootCaCertificate = CertificateUtils.getRootCaIfPossible(certificate);
        assertThat(rootCaCertificate).isNotEmpty();

        certificateUtilsMockedStatic.verify(() -> CertificateUtils.getRootCaFromJdkTrustedCertificates(certificate), times(1));
    }
}

This method resolves most of the issues I had.
Do you think I have incorrectly used mocking with mockstatic or do you think there is a bug in the latest version? I never had this issue with the initial test until the latest version.

So basically what I wonder is should I rewrite my tests or do you think it is a bug and I should wait for a new mockito version? I have the feeling it

Looking at the PR of #2506 it gives me the idea that I was incorrectly using mockstatic and therefor with the latest version I am getting an exception right?

@temp-droid
Copy link
Contributor

Hey @Hakky54 ,

Do you think I have incorrectly used mocking with mockstatic or do you think there is a bug in the latest version? I never had this issue with the initial test until the latest version.

It looks to me your implementation was wrong (because you don't return a List), and your attempt to fix it (casting the invocation.getMock() to a List) avoids the type verification that would throw an error.

And because you mock it again after (certificateUtilsMockedStatic.when(() -> CertificateUtils.getRootCaFromAuthorityInfoAccessExtensionIfPresent(any(X509Certificate.class))) .thenReturn(Collections.emptyList());), it overrides your previous configuration and makes the test pass.
If you remove the certificateUtilsMockedStatic.when( part, your test should still pass. But in that case your first mock should return a valid List, otherwise you will face java.lang.ClassCastException: class java.lang.Class cannot be cast to class java.util.List.

In the end, you could use:

        try (MockedStatic<CertificateUtils> certificateUtilsMockedStatic = mockStatic(CertificateUtils.class, invocation -> {
            Method method = invocation.getMethod();
            if ("getRootCaFromAuthorityInfoAccessExtensionIfPresent".equals(method.getName())) {
                return Collections.emptyList();
            } else {
                return invocation.callRealMethod();
            }
        })) {

            X509Certificate certificate = (X509Certificate) certificates.get(certificates.size() - 1);

@Hakky54
Copy link
Author

Hakky54 commented Jan 2, 2022

Thank you for taking your time to look at this issue, and yes I totally agree with you that my test setup was not correct and adjusting it from my side fixes all the failing tests. I think we can close this issue as it is not an issue

@Hakky54 Hakky54 closed this as completed Jan 2, 2022
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

3 participants