it-swarm.com.de

Verspottung statischer Methoden mit Mockito

Ich habe eine Fabrik geschrieben, um Java.sql.Connection-Objekte zu produzieren:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

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

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

263
Naftuli Kay

Verwenden Sie PowerMockito auf Mockito.

Beispielcode:

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

    }

Mehr Informationen:

279
MariuszS

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

Die Wrapper-Objekte werden zu Fassaden der realen statischen Klassen, und Sie testen diese nicht.

Ein Wrapper-Objekt könnte ungefähr so ​​aussehen

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

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

Schließlich kann Ihre getestete Klasse dieses Singleton-Objekt verwenden, indem Sie zum Beispiel 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 nicht direkt mit statischen Methoden verwenden.

Wenn Sie CDI verwenden und die @Inject-Annotation verwenden können, ist es noch einfacher. __ Machen Sie Ihre Wrapper-Bean zu @ApplicationScoped, lassen Sie das Ding als Collaborator injizieren (Sie benötigen nicht einmal chaotische Konstruktoren zum Testen), und mach weiter mit dem Spott.

49
99Sono

Wie bereits erwähnt, können Sie statische Methoden nicht mit Mockito simulieren. 

Wenn das Ändern Ihres Testframeworks keine Option ist, können Sie Folgendes tun:

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

16
ChrisM

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

Und ich muss nicht BDDMockito verwenden. 

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

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

Für z. :

securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

Verwenden Sie für den obigen Code, wenn Sie die MessageDigest-Klasse simulieren 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 der Code befindet. 

@PrepareForTest(CustomObjectRule.class)

Und dann verspotten Sie die Methode: 

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

Um die statische Methode zu simulieren, sollten Sie einen Powermock-Look verwenden: https://github.com/powermock/powermock/wiki/MockStatic . Mockito bietet diese Funktion nicht.

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

6
marek.kapowicki

Sie können es mit ein bisschen Refactoring machen:

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 MySQLDatabaseConnectionFactory erweitern, um eine gespielte Verbindung zurückzugeben, Assertions zu den Parametern vorzunehmen, usw.

Die erweiterte Klasse kann sich innerhalb des Testfalls befinden, wenn sie sich in demselben Paket befindet (was ich Ihnen empfehlen sollte).

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

Ich habe auch eine Kombination aus Mockito und AspectJ geschrieben: https://github.com/iirekm/varia/tree/develop/ajmock

Ihr Beispiel wird:

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

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

Beispiel (extrahiert aus ihren 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;
        }
    }
}

Ihr Ziel ist nicht die direkte Unterstützung von statischem Mocking, sondern die Verbesserung der öffentlichen APIs, sodass andere Bibliotheken wie Powermockito nicht auf interne APIs angewiesen sind oder Mockito-Code direkt duplizieren müssen. ( Quelle )

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

1
David Miguel

JMockit Framework verwenden . Es hat für mich funktioniert. Sie müssen keine Anweisungen schreiben, um die DBConenction.getConnection () - Methode zu verspotten. Nur der unten stehende Code reicht aus.

@Mock unten ist das Paket 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