web-dev-qa-db-fra.com

Se moquer des méthodes statiques avec Mockito

J'ai écrit une usine pour produire des objets Java.sql.Connection:

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 se moquer d'une méthode statique. J'utilise JUnit 4 et Mockito pour mes cas de test. Existe-t-il un bon moyen de simuler/vérifier ce cas d'utilisation spécifique?

263
Naftuli Kay

Utilisez PowerMockito sur 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'information:

279
MariuszS

La stratégie typique d'esquive des méthodes statiques que vous n'avez aucun moyen d'éviter d'utiliser consiste à créer des objets encapsulés et à utiliser les objets encapsuleurs à la place.

Les objets wrapper deviennent des façades pour les vraies classes statiques et vous ne les testez pas.

Un objet wrapper pourrait être quelque chose comme

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

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

Enfin, votre classe testée peut utiliser cet objet singleton, par exemple, en ayant un constructeur par défaut pour une utilisation réelle:

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 vous avez ici une classe qui peut facilement être testée, car vous n'utilisez pas directement une classe avec des méthodes statiques.

Si vous utilisez CDI et pouvez utiliser l'annotation @Inject, alors c'est encore plus simple… .. Créez simplement votre bean Wrapper @ApplicationScoped, faites l'injecter en tant que collaborateur (vous n'avez même pas besoin de constructeurs désordonnés pour les tests), et continuez avec les moqueries.

49
99Sono

Comme mentionné précédemment, vous ne pouvez pas vous moquer des méthodes statiques avec Mockito. 

Si la modification de votre structure de test n’est pas une option, vous pouvez procéder comme suit:

Créez une interface pour DriverManager, simulez cette interface, injectez-la via une sorte d'injection de dépendance et vérifiez-la. 

16
ChrisM

J'ai eu un problème similaire. La réponse acceptée ne fonctionnait pas pour moi jusqu'à ce que je modifie: @PrepareForTest(TheClassThatContainsStaticMethod.class), selon la documentation de PowerMock pour 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 de test:

@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());
    }
}
13
6324

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

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)

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

public class CustomObjectRule {

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

}

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

@PrepareForTest(CustomObjectRule.class)

Et puis moquez la méthode: 

PowerMockito.mockStatic(MessageDigest.class);
PowerMockito.when(MessageDigest.getInstance(Mockito.anyString()))
      .thenThrow(new RuntimeException());
6
some random guy

Pour vous moquer de la méthode statique, vous devez utiliser Powermock dans: https://github.com/powermock/powermock/wiki/MockStatic . Mockito ne fournit pas cette fonctionnalité.

Vous pouvez lire un article de Nice sur mockito: http://refcardz.dzone.com/refcardz/mockito

6
marek.kapowicki

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 renvoyer une connexion simulée, faire des affirmations sur les paramètres, etc.

La classe étendue peut résider dans le scénario de test, si elle se trouve dans le même package (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
Fermin Silva

J'ai aussi écrit une combinaison de Mockito et AspectJ: https://github.com/iirekm/varia/tree/develop/ajmock

Votre exemple devient:

when(() -> DriverManager.getConnection(...)).thenReturn(...);
3
iirekm

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

Exemple (extrait de leurs tests ):

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 prendre en charge directement le mocking statique, mais d'améliorer ses API publiques afin que d'autres bibliothèques, telles que Powermockito , n'aient pas à s'appuyer sur des API internes ni à dupliquer directement du code Mockito. ( la source )

Clause de non-responsabilité: l'équipe Mockito pense que l'enfer est pavé de méthodes statiques. Cependant, le travail de Mockito n'est pas de protéger votre code contre les méthodes statiques. Si vous n'aimez pas votre équipe se moquer de manière statique, arrêtez d'utiliser Powermockito dans votre organisation. Mockito doit évoluer en tant que boîte à outils avec une vision éclairée sur la manière dont les tests Java doivent être écrits (par exemple, ne vous moquez pas de la statistique !!!). Cependant, Mockito n'est pas dogmatique. Nous ne voulons pas bloquer les cas d'utilisation non recommandés comme les moqueries statiques. Ce n'est tout simplement pas notre travail.

1
David Miguel

Utiliser le framework JMockit . Cela a fonctionné pour moi. Vous n'avez pas à écrire d'instructions pour vous moquer de la méthode DBConenction.getConnection (). Le code ci-dessous suffit.

@Mock ci-dessous est le paquet mockit.Mock

Connection jdbcConnection = Mockito.mock(Connection.class);

MockUp<DBConnection> mockUp = new MockUp<DBConnection>() {

            DBConnection singleton = new DBConnection();

            @Mock
            public DBConnection getInstance() { 
                return singleton;
            }

            @Mock
            public Connection getConnection() {
                return jdbcConnection;
            }
         };
0
Zlatan