Se moquer des méthodes statiques avec Mockito

j'ai écrit une usine pour produire java.sql.Connection objets:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

    @Override public Connection getConnection() {
        try {
            return DriverManager.getConnection(...);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

j'aimerais valider les paramètres passés à DriverManager.getConnection , mais je ne sais pas comment simuler une méthode statique. J'utilise JUnit 4 et Mockito pour mes tests. Est-il une bonne façon de se moquer/vérifier ce cas d'utilisation?

239
demandé sur Naftuli Kay 2014-01-14 07:08:53

9 réponses

Utiliser PowerMockito sur le dessus de Mockito.

exemple de code:

@RunWith(PowerMockRunner.class)
@PrepareForTest(DriverManager.class)
public class Mocker {

    @Test
    public void testName() throws Exception {

        //given
        PowerMockito.mockStatic(DriverManager.class);
        BDDMockito.given(DriverManager.getConnection(...)).willReturn(...);

        //when
        sut.execute();

        //then
        PowerMockito.verifyStatic();
        DriverManager.getConnection(...);

    }

plus d'informations:

253
répondu MariuszS 2018-05-30 09:24:10

la stratégie typique pour esquiver les méthodes statiques que vous n'avez aucun moyen d'éviter d'utiliser, est en créant des objets enveloppés et en utilisant les objets enveloppants à la place.

les objets d'emballage deviennent des façades aux classes statiques réelles, et vous ne les testez pas.

un objet d'emballage pourrait être quelque chose comme

public class Slf4jMdcWrapper {
    public static final Slf4jMdcWrapper SINGLETON = new Slf4jMdcWrapper();

    public String myApisToTheSaticMethodsInSlf4jMdcStaticUtilityClass() {
        return MDC.getWhateverIWant();
    }
}

Enfin, votre classe sous test peut utiliser cet objet singleton, par exemple, avoir un défaut constructeur pour usage réel:

public class SomeClassUnderTest {
    final Slf4jMdcWrapper myMockableObject;

    /** constructor used by CDI or whatever real life use case */
    public myClassUnderTestContructor() {
        this.myMockableObject = Slf4jMdcWrapper.SINGLETON;
    }

    /** constructor used in tests*/
    myClassUnderTestContructor(Slf4jMdcWrapper myMock) {
        this.myMockableObject = myMock;
    }
}

et ici vous avez une classe qui peut facilement être testé, parce que vous n'utilisez pas directement une classe avec des méthodes statiques.

si vous utilisez CDI et pouvez faire usage de l'annotation @Inject alors il est encore plus facile. Il suffit de faire votre Wrapper bean @ApplicationScoped, obtenir cette chose injectée comme un collaborateur (vous n'avez même pas besoin de constructeurs désordonnés pour les tests), et continuer avec la moquerie.

45
répondu 99Sono 2017-07-25 14:07:46

comme mentionné ci-dessus, vous ne pouvez pas vous moquer des méthodes statiques avec mockito.

si changer votre framework de test n'est pas une option, vous pouvez faire ce qui suit:

créer une interface pour DriverManager, mock cette interface, l'injecter via une sorte d'injection de dépendance et de vérifier sur cette mock.

16
répondu ChrisM 2014-01-14 13:29:03

j'ai eu un problème similaire. La réponse acceptée n'a pas fonctionné pour moi, jusqu'à ce que je fasse le changement: @PrepareForTest(TheClassThatContainsStaticMethod.class) , selon Powermock's documentation for mockStatic .

Et je n'ai pas à utiliser BDDMockito .

ma classe:

public class SmokeRouteBuilder {
    public static String smokeMessageId() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            log.error("Exception occurred while fetching localhost address", e);
            return UUID.randomUUID().toString();
        }
    }
}

ma classe d'essai:

@RunWith(PowerMockRunner.class)
@PrepareForTest(SmokeRouteBuilder.class)
public class SmokeRouteBuilderTest {
    @Test
    public void testSmokeMessageId_exception() throws UnknownHostException {
        UUID id = UUID.randomUUID();

        mockStatic(InetAddress.class);
        mockStatic(UUID.class);
        when(InetAddress.getLocalHost()).thenThrow(UnknownHostException.class);
        when(UUID.randomUUID()).thenReturn(id);

        assertEquals(id.toString(), SmokeRouteBuilder.smokeMessageId());
    }
}
9
répondu 6324 2018-07-05 13:31:12

pour simuler la méthode statique, vous devriez utiliser un regard Powermock: https://github.com/powermock/powermock/wiki/MockStatic . Mockito ne fournit pas cette fonctionnalité.

vous pouvez lire nice un article à propos de mockito: http://refcardz.dzone.com/refcardz/mockito

6
répondu marek.kapowicki 2017-05-19 10:46:08

Vous pouvez le faire avec un peu de refactoring:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

    @Override public Connection getConnection() {
        try {
            return _getConnection(...some params...);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    //method to forward parameters, enabling mocking, extension, etc
    Connection _getConnection(...some params...) throws SQLException {
        return DriverManager.getConnection(...some params...);
    }
}

ensuite vous pouvez étendre votre classe MySQLDatabaseConnectionFactory pour retourner une connexion moquée, faire des assertions sur les paramètres, etc.

la classe étendue peut résider dans le cas d'essai, si elle est située dans le même colis (ce que je vous encourage à faire)

public class MockedConnectionFactory extends MySQLDatabaseConnectionFactory {

    Connection _getConnection(...some params...) throws SQLException {
        if (some param != something) throw new InvalidParameterException();

        //consider mocking some methods with when(yourMock.something()).thenReturn(value)
        return Mockito.mock(Connection.class);
    }
}
5
répondu Fermin Silva 2015-08-12 21:35:33

Observation: lorsque vous appelez méthode statique dans une entité statique, vous devez changer la classe dans @PrepareForTest.

Pour par exemple:

securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

pour le code ci-dessus si vous devez vous moquer de la classe MessageDigest, utilisez

@PrepareForTest(MessageDigest.class)

tandis que si vous avez quelque chose comme ci-dessous :

public class CustomObjectRule {

    object = DatatypeConverter.printHexBinary(MessageDigest.getInstance(SECURITY_ALGORITHM)
             .digest(message.getBytes(ENCODING)));

}

alors, vous devez préparer la classe dans laquelle ce code réside.

@PrepareForTest(CustomObjectRule.class)

et ensuite moquer la méthode:

PowerMockito.mockStatic(MessageDigest.class);
PowerMockito.when(MessageDigest.getInstance(Mockito.anyString()))
      .thenThrow(new RuntimeException());
5
répondu some random guy 2017-10-30 18:55:58

j'ai aussi écrit une combinaison de Mockito et AspectJ: https://github.com/iirekm/misc/tree/master/ajmock

votre exemple devient:

when(() -> DriverManager.getConnection(...)).thenReturn(...);
3
répondu iirekm 2017-11-02 09:35:05

Mockito ne peut pas capturer les méthodes statiques, mais depuis Mockito 2.14.0 vous pouvez le simuler en créant des instances d'invocation de méthodes statiques.

exemple (extrait de leurs épreuves ):

public class StaticMockingExperimentTest extends TestBase {

    Foo mock = Mockito.mock(Foo.class);
    MockHandler handler = Mockito.mockingDetails(mock).getMockHandler();
    Method staticMethod;
    InvocationFactory.RealMethodBehavior realMethod = new InvocationFactory.RealMethodBehavior() {
        @Override
        public Object call() throws Throwable {
            return null;
        }
    };

    @Before
    public void before() throws Throwable {
        staticMethod = Foo.class.getDeclaredMethod("staticMethod", String.class);
    }

    @Test
    public void verify_static_method() throws Throwable {
        //register staticMethod call on mock
        Invocation invocation = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "some arg");
        handler.handle(invocation);

        //verify staticMethod on mock
        //Mockito cannot capture static methods so we will simulate this scenario in 3 steps:
        //1. Call standard 'verify' method. Internally, it will add verificationMode to the thread local state.
        //  Effectively, we indicate to Mockito that right now we are about to verify a method call on this mock.
        verify(mock);
        //2. Create the invocation instance using the new public API
        //  Mockito cannot capture static methods but we can create an invocation instance of that static invocation
        Invocation verification = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "some arg");
        //3. Make Mockito handle the static method invocation
        //  Mockito will find verification mode in thread local state and will try verify the invocation
        handler.handle(verification);

        //verify zero times, method with different argument
        verify(mock, times(0));
        Invocation differentArg = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "different arg");
        handler.handle(differentArg);
    }

    @Test
    public void stubbing_static_method() throws Throwable {
        //register staticMethod call on mock
        Invocation invocation = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "foo");
        handler.handle(invocation);

        //register stubbing
        when(null).thenReturn("hey");

        //validate stubbed return value
        assertEquals("hey", handler.handle(invocation));
        assertEquals("hey", handler.handle(invocation));

        //default null value is returned if invoked with different argument
        Invocation differentArg = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "different arg");
        assertEquals(null, handler.handle(differentArg));
    }

    static class Foo {

        private final String arg;

        public Foo(String arg) {
            this.arg = arg;
        }

        public static String staticMethod(String arg) {
            return "";
        }

        @Override
        public String toString() {
            return "foo:" + arg;
        }
    }
}

leur objectif n'est pas de soutenir directement la moquerie statique, mais d'améliorer ses API publiques de sorte que les autres bibliothèques, comme Powermockito , ne doivent pas compter sur des APIs ou directement doivent dupliquer certains Mockito code. ( source )

avertissement: L'équipe de Mockito pense que la route de l'enfer est pavée avec des méthodes statiques. Cependant, le travail de Mockito n'est pas de protéger votre code des méthodes statiques. Si vous n'aimez pas que votre équipe fasse des moqueries statiques, arrêtez D'utiliser Powermockito dans votre organisation. Mockito doit évoluer comme une boîte à outils avec une vision opiniâtre sur la façon dont les tests Java devraient être écrits (par exemple, ne pas maquette statique!!!). Cependant, Mockito n'est pas dogmatique. Nous ne voulons pas bloquer les cas d'usage non recommandé comme les moqueries statiques. C'est juste pas notre travail.

1
répondu David Miguel 2018-08-19 21:11:55