Verspotten statischer Methoden mit Mockito

372

Ich habe eine Fabrik geschrieben, um java.sql.ConnectionObjekte herzustellen :

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

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

Ich möchte die übergebenen Parameter überprüfen DriverManager.getConnection, weiß aber nicht, wie ich eine statische Methode verspotten soll. Ich verwende JUnit 4 und Mockito für meine Testfälle. Gibt es eine gute Möglichkeit, diesen speziellen Anwendungsfall zu verspotten / zu verifizieren?

Naftuli Kay
quelle
1
würde das helfen? stackoverflow.com/questions/19464975/…
Sasankad
5
Sie können nicht mit mockito durch desing :)
MariuszS
25
@MariuszS Es ist nicht beabsichtigt, dass Mockito (oder EasyMock oder jMock) keine Verspottungsmethoden unterstützt static, sondern aus Versehen . Diese Einschränkung (zusammen mit keiner Unterstützung für das Verspotten von finalKlassen / Methoden oder new-ed-Objekten) ist eine natürliche (aber unbeabsichtigte) Folge des Ansatzes zum Implementieren von Mocking, bei dem neue Klassen dynamisch erstellt werden, die den zu verspottenden Typ implementieren / erweitern. Andere Spottbibliotheken verwenden andere Ansätze, die diese Einschränkungen vermeiden. Dies geschah auch in der .NET-Welt.
Rogério
2
@ Rogério Danke für die Erklärung. github.com/mockito/mockito/wiki/FAQ Kann ich statische Methoden verspotten? Nein. Mockito bevorzugt Objektorientierung und Abhängigkeitsinjektion gegenüber statischem, prozeduralem Code, der schwer zu verstehen und zu ändern ist. Es gibt auch einige Design hinter dieser Einschränkung :)
MariuszS
17
@MariuszS Ich habe gelesen, dass der Versuch, legitime Anwendungsfälle zu verwerfen, anstatt das Tool zuzulassen, Einschränkungen aufweist, die nicht (einfach) und ohne Begründung beseitigt werden können. Übrigens, hier ist eine solche Diskussion für den entgegengesetzten Standpunkt mit Referenzen.
Rogério

Antworten:

350

Verwenden Sie PowerMockito über Mockito.

Beispielcode:

@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(...);

    }

Mehr Informationen:

MariuszS
quelle
4
Während dies theoretisch funktioniert, fällt es in der Praxis schwer ...
Naftuli Kay
38
Leider ist der große Nachteil die Notwendigkeit von PowerMockRunner.
Innokenty
18
sut.execute ()? Meint?
TejjD
4
System Under Test, die Klasse, für die DriverManager nachgebildet werden muss. kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs
MariuszS
8
Zu Ihrer Information, wenn Sie bereits JUnit4 verwenden, können Sie dies tun @RunWith(PowerMockRunner.class)und darunter @PowerMockRunnerDelegate(JUnit4.class).
EM-Creations
71

Die typische Strategie, um statischen Methoden auszuweichen, die Sie nicht vermeiden können, besteht darin, umschlossene Objekte zu erstellen und stattdessen die Wrapper-Objekte zu verwenden.

Die Wrapper-Objekte werden zu Fassaden für die realen statischen Klassen, und Sie testen diese nicht.

Ein Wrapper-Objekt könnte so etwas wie sein

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

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

Schließlich kann Ihre zu testende Klasse dieses Singleton-Objekt verwenden, indem Sie beispielsweise einen Standardkonstruktor für die Verwendung im realen Leben haben:

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;
    }
}

Und hier haben Sie eine Klasse, die leicht getestet werden kann, da Sie eine Klasse mit statischen Methoden nicht direkt verwenden.

Wenn Sie CDI verwenden und die Annotation @Inject verwenden können, ist dies noch einfacher. Machen Sie einfach Ihre Wrapper-Bean @ApplicationScoped, lassen Sie das Ding als Kollaborateur injizieren (Sie brauchen nicht einmal unordentliche Konstruktoren zum Testen) und fahren Sie mit dem Verspotten fort.

99Sono
quelle
3
Ich habe ein Tool erstellt, um automatisch Java 8 "Mixin" -Schnittstellen zu generieren, die statische Aufrufe umschließen : github.com/aro-tech/interface-it Die generierten Mixins können wie jede andere Schnittstelle verspottet werden oder wenn Ihre zu testende Klasse die "implementiert" Schnittstelle Sie können jede seiner Methoden in einer Unterklasse für den Test überschreiben.
Aro_tech
25

Ich hatte ein ähnliches Problem. Die akzeptierte Antwort hat bei mir nicht funktioniert, bis ich die Änderung vorgenommen habe: @PrepareForTest(TheClassThatContainsStaticMethod.class)Laut PowerMock-Dokumentation für mockStatic .

Und ich muss nicht verwenden BDDMockito .

Meine Klasse:

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

Meine Testklasse:

@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
quelle
Nicht in der Lage herauszufinden? .MockStatic und? .Wenn derzeit mit JUnit 4
Teddy
PowerMock.mockStatic & Mockito.wenn nicht zu funktionieren scheint.
Teddy
Für alle, die dies später sehen, musste ich PowerMockito.mockStatic (StaticClass.class) eingeben;
Thinkereer
Sie müssen Powermock-Api-Mockito Maven Arterfact enthalten.
PeterS
23

Wie bereits erwähnt, können Sie statische Methoden mit mockito nicht verspotten.

Wenn das Ändern Ihres Testframeworks nicht möglich ist, können Sie Folgendes tun:

Erstellen Sie eine Schnittstelle für DriverManager, verspotten Sie diese Schnittstelle, fügen Sie sie über eine Art Abhängigkeitsinjektion ein und überprüfen Sie diese.

ChrisM
quelle
7

Beobachtung: Wenn Sie eine statische Methode innerhalb einer statischen Entität aufrufen, müssen Sie die Klasse in @PrepareForTest ändern.

Zum Beispiel:

securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

Verwenden Sie für den obigen Code, wenn Sie die MessageDigest-Klasse verspotten müssen

@PrepareForTest(MessageDigest.class)

Während, wenn Sie etwas wie unten haben:

public class CustomObjectRule {

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

}

Dann müssen Sie die Klasse vorbereiten, in der sich dieser Code befindet.

@PrepareForTest(CustomObjectRule.class)

Und dann verspotten Sie die Methode:

PowerMockito.mockStatic(MessageDigest.class);
PowerMockito.when(MessageDigest.getInstance(Mockito.anyString()))
      .thenThrow(new RuntimeException());
ein zufälliger Typ
quelle
Ich schlug meinen Kopf gegen die Wand und versuchte herauszufinden, warum meine statische Klasse nicht spottete. Sie würden denken, in allen Tutorials in den Interwebs wäre ONE auf mehr als den Bare-Bones-Anwendungsfall eingegangen.
SoftwareSavant
6

Sie können es mit ein wenig Refactoring tun:

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...);
    }
}

Dann können Sie Ihre Klasse erweitern MySQLDatabaseConnectionFactory , um eine verspottete Verbindung zurückzugeben, Aussagen zu den Parametern zu machen usw.

Die erweiterte Klasse kann sich im Testfall befinden, wenn sie sich im selben Paket befindet (wozu ich Sie ermutige).

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
quelle
6

Um eine statische Methode zu verspotten, sollten Sie einen Powermock-Look verwenden: https://github.com/powermock/powermock/wiki/MockStatic . Mockito bietet diese Funktionalität nicht an.

Sie können einen Artikel über Mockito lesen: http://refcardz.dzone.com/refcardz/mockito

marek.kapowicki
quelle
2
Bitte verlinken Sie nicht auf eine Website. Die Antworten sollten tatsächlich verwendbare Antworten enthalten. Wenn die Site ausfällt oder sich ändert, ist die Antwort nicht mehr gültig.
the_new_mr
6

Mockito kann keine statischen Methoden erfassen, aber seit Mockito 2.14.0 können Sie es simulieren, indem Sie Aufrufinstanzen statischer Methoden erstellen.

Beispiel (aus ihren Tests extrahiert ):

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;
        }
    }
}

Ihr Ziel ist es nicht, statisches Mocking direkt zu unterstützen, sondern die öffentlichen APIs zu verbessern, damit andere Bibliotheken wie Powermockito nicht auf interne APIs angewiesen sind oder direkt Mockito-Code duplizieren müssen. ( Quelle )

Haftungsausschluss: Das Mockito-Team glaubt, dass der Weg zur Hölle mit statischen Methoden gepflastert ist. Mockitos Aufgabe ist es jedoch nicht, Ihren Code vor statischen Methoden zu schützen. Wenn Sie nicht möchten, dass Ihr Team statische Verspottungen ausführt, beenden Sie die Verwendung von Powermockito in Ihrer Organisation. Mockito muss sich als Toolkit mit einer Vision weiterentwickeln, wie Java-Tests geschrieben werden sollten (z. B. keine Statik verspotten !!!). Mockito ist jedoch nicht dogmatisch. Wir möchten nicht empfohlene Anwendungsfälle wie statisches Verspotten blockieren. Es ist einfach nicht unser Job.

David Miguel
quelle
1

Da diese Methode statisch ist, verfügt sie bereits über alles, was Sie zur Verwendung benötigen, sodass der Zweck des Verspottens zunichte gemacht wird. Das Verspotten der statischen Methoden wird als schlechte Praxis angesehen.

Wenn Sie dies versuchen, bedeutet dies, dass etwas mit der Art und Weise, wie Sie Tests durchführen möchten, nicht stimmt.

Natürlich können Sie PowerMockito oder ein anderes Framework verwenden, das dazu in der Lage ist, aber versuchen Sie, Ihren Ansatz zu überdenken.

Beispiel: Versuchen Sie, die Objekte zu verspotten / bereitzustellen, die diese statische Methode stattdessen verwendet.

Benas
quelle
0

Verwenden Sie das JMockit-Framework . Es hat bei mir funktioniert. Sie müssen keine Anweisungen schreiben, um die Methode DBConenction.getConnection () zu verspotten. Nur der folgende Code ist genug.

@Mock unten ist mockit.Mock-Paket

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
quelle