Se moquer des méthodes statiques avec Mockito

374

J'ai écrit une usine pour produire des java.sql.Connectionobjets:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

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

Je voudrais 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 se moquer / de vérifier ce cas d'utilisation spécifique?

Naftuli Kay
la source
1
cela aiderait-il? stackoverflow.com/questions/19464975/…
sasankad
5
Vous ne pouvez pas avec mockito par desing :)
MariuszS
25
@MariuszS Ce n'est pas par conception que Mockito (ou EasyMock ou jMock) ne prend pas en charge les staticméthodes de simulation , mais par accident . Cette limitation (ainsi que l'absence de prise en charge des finalclasses / méthodes de newsimulation ou des objets -ed) est une conséquence naturelle (mais non intentionnelle) de l'approche utilisée pour implémenter la simulation, où de nouvelles classes sont créées dynamiquement qui implémentent / étendent le type à simuler; d'autres bibliothèques de simulation utilisent d'autres approches qui évitent ces limitations. Cela s'est également produit dans le monde .NET.
Rogério
2
@ Rogério Merci pour l'explication. github.com/mockito/mockito/wiki/FAQ Puis-je me moquer des méthodes statiques? Non. Mockito préfère l'orientation des objets et l'injection de dépendances au code procédural statique difficile à comprendre et à modifier. Il y a aussi du design derrière cette limitation :)
MariuszS
17
@MariuszS J'ai lu que pour tenter de rejeter des cas d'utilisation légitimes au lieu d'admettre l'outil, il y avait des limites qui ne pouvaient pas (facilement) être supprimées, et sans fournir de justification raisonnée. BTW, voici une telle discussion pour le point de vue opposé, avec des références.
Rogério

Réponses:

350

Utilisez PowerMockito sur Mockito.

Exemple de code:

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

    @Test
    public void shouldVerifyParameters() throws Exception {

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

        //when
        sut.execute(); // System Under Test (sut)

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

    }

Plus d'information:

MariuszS
la source
4
Bien que cela fonctionne en théorie, avoir du mal à la pratique ...
Naftuli Kay
38
Malheureusement, l'énorme inconvénient est le besoin de PowerMockRunner.
Innokenty
18
sut.execute ()? Veux dire?
TejjD
4
System Under Test, la classe qui nécessite une maquette de DriverManager. kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs
MariuszS
8
Pour info, si vous utilisez déjà JUnit4, vous pouvez le faire @RunWith(PowerMockRunner.class)et en dessous @PowerMockRunnerDelegate(JUnit4.class).
EM-Creations
71

La stratégie typique pour esquiver les méthodes statiques que vous n'avez aucun moyen d'éviter d'utiliser est de créer des objets encapsulés et d'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 en ayant, par exemple, 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 ici, vous avez 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, c'est encore plus facile. Faites simplement votre bean Wrapper @ApplicationScoped, obtenez cette chose injectée en tant que collaborateur (vous n'avez même pas besoin de constructeurs désordonnés pour les tests), et continuez avec la moquerie.

99Sono
la source
3
J'ai créé un outil pour générer automatiquement des interfaces "mixin" Java 8 qui encapsulent les appels statiques: github.com/aro-tech/interface-it Les mixins générés peuvent être simulés comme n'importe quelle autre interface, ou si votre classe sous test "implémente" la interface, vous pouvez remplacer n'importe laquelle de ses méthodes dans une sous-classe pour le test.
aro_tech
25

J'ai eu un problème similaire. La réponse acceptée n'a pas fonctionné pour moi, jusqu'à ce que j'effectue le changement :,@PrepareForTest(TheClassThatContainsStaticMethod.class) selon la documentation de PowerMock pour mockStatic .

Et je n'ai pas besoin de l'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());
    }
}
6324
la source
Impossible de comprendre? .MockStatic et? .When actuellement avec JUnit 4
Teddy
PowerMock.mockStatic & Mockito.when ne semble pas fonctionner.
Teddy
Pour tous ceux qui le verront plus tard, pour moi, je devais taper PowerMockito.mockStatic (StaticClass.class);
thinkereer
Vous devez inclure l'artefact powermock-api-mockito maven.
PeterS
23

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

Si la modification de votre infrastructure de test n'est pas une option, vous pouvez effectuer les opérations suivantes:

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

ChrisM
la source
7

Observation: lorsque vous appelez une méthode statique au sein d'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 simuler la classe MessageDigest, utilisez

@PrepareForTest(MessageDigest.class)

Alors 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 réside ce code.

@PrepareForTest(CustomObjectRule.class)

Et puis se moquer de la méthode:

PowerMockito.mockStatic(MessageDigest.class);
PowerMockito.when(MessageDigest.getInstance(Mockito.anyString()))
      .thenThrow(new RuntimeException());
un gars au hasard
la source
Je me cognais la tête contre le mur en essayant de comprendre pourquoi ma classe statique ne se moquait pas. On pourrait penser que dans tous les tutoriels sur les interwebs, ONE serait allé plus loin que le cas d'utilisation bare-bones.
SoftwareSavant
6

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

La classe étendue peut résider dans le cas 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);
    }
}
Fermin Silva
la source
6

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

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

marek.kapowicki
la source
2
Veuillez ne pas créer de lien vers un site Web. Les réponses doivent inclure des réponses réellement utilisables. Si le site tombe en panne ou change, la réponse n'est plus valable.
the_new_mr
6

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

Avertissement: L'équipe Mockito pense que la route de l'enfer est pavée de 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 se moque statiquement, arrêtez d'utiliser Powermockito dans votre organisation. Mockito doit évoluer en tant que boîte à outils avec une vision d'opinion sur la façon dont les tests Java doivent être écrits (par exemple, ne vous moquez pas des statistiques !!!). Cependant, Mockito n'est pas dogmatique. Nous ne voulons pas bloquer les cas d'utilisation non recommandés comme la simulation statique. Ce n'est tout simplement pas notre travail.

David Miguel
la source
1

Comme cette méthode est statique, elle a déjà tout ce dont vous avez besoin pour l'utiliser, donc elle va à l'encontre du but de la moquerie. Se moquer des méthodes statiques est considéré comme une mauvaise pratique.

Si vous essayez de le faire, cela signifie qu'il y a un problème avec la façon dont vous souhaitez effectuer les tests.

Bien sûr, vous pouvez utiliser PowerMockito ou tout autre framework capable de le faire, mais essayez de repenser votre approche.

Par exemple: essayez de simuler / fournir les objets, que cette méthode statique consomme à la place.

Benas
la source
0

Utilisez le framework JMockit . Ça a marché pour moi. Il n'est pas nécessaire d'écrire des instructions pour se moquer de la méthode DBConenction.getConnection (). Le code ci-dessous suffit.

@Mock ci-dessous est le package 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;
            }
         };
Zlatan
la source