Wyśmiewanie metod statycznych za pomocą Mockito


371

Napisałem fabrykę do produkcji java.sql.Connectionprzedmiotów:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

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

Chciałbym zweryfikować przekazane parametry DriverManager.getConnection, ale nie wiem, jak wyśmiewać metodę statyczną. Używam JUnit 4 i Mockito do moich przypadków testowych. Czy istnieje dobry sposób na wyszydzenie / zweryfikowanie tego konkretnego przypadku użycia?



5
Nie możesz z mockito , desing :)
MariuszS

25
@MariuszS Mockito (lub EasyMock lub jMock) nie jest z założenia projektowany static, ale przez przypadek . Ograniczenie to (wraz z brakiem wsparcia dla kpiących finalklas / metod lub newobiektów -ed) jest naturalną (ale niezamierzoną) konsekwencją podejścia zastosowanego do implementacji kpienia, w którym dynamicznie tworzone są nowe klasy, które implementują / rozszerzają kpiący typ; inne kpiące biblioteki używają innych podejść, które omijają te ograniczenia. Stało się to również w świecie .NET.
Rogério

2
@ Rogério Dziękuję za wyjaśnienie. github.com/mockito/mockito/wiki/FAQ Czy mogę kpić z metod statycznych? Nie. Mockito woli wprowadzanie orientacji obiektu i wstrzykiwania zależności od statycznego, proceduralnego kodu, który jest trudny do zrozumienia i zmiany. Za tym ograniczeniem kryje się również trochę designu
MariuszS

17
@MariuszS Przeczytałem, że próba odrzucenia uzasadnionych przypadków użycia zamiast przyznania się do tego narzędzia ma ograniczenia, których nie można (łatwo) usunąć i bez podania uzasadnionego uzasadnienia. BTW, oto taka dyskusja dla przeciwnego punktu widzenia, z odniesieniami.
Rogério,

Odpowiedzi:


350

Użyj PowerMockito na Mockito.

Przykładowy kod:

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

    }

Więcej informacji:


4
Chociaż działa to w teorii, trudno jest w praktyce ...
Naftuli Kay

38
Niestety ogromną wadą tego jest potrzeba PowerMockRunner.
Innokenty,

18
sut.execute ()? Znaczy?
TejjD,

4
Testowany system: klasa wymagająca fałszywego sterownika DriverManager. kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs
MariuszS

8
Do Twojej wiadomości, jeśli już używasz JUnit4, możesz to zrobić @RunWith(PowerMockRunner.class)i poniżej @PowerMockRunnerDelegate(JUnit4.class).
EM-Creations,

71

Typową strategią unikania metod statycznych, których nie można w żaden sposób uniknąć, jest tworzenie zawijanych obiektów i używanie zamiast nich obiektów opakowujących.

Obiekty opakowania stają się fasadami prawdziwych klas statycznych, a ty ich nie testujesz.

Obiekt opakowania może być podobny

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

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

Wreszcie, twoja testowana klasa może korzystać z tego obiektu singleton, na przykład mając domyślnego konstruktora do użytku w prawdziwym życiu:

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

A tutaj masz klasę, którą można łatwo przetestować, ponieważ nie używasz bezpośrednio klasy za pomocą metod statycznych.

Jeśli używasz CDI i możesz skorzystać z adnotacji @Inject, jest to jeszcze łatwiejsze. Po prostu stwórz fasolę Wrapper @ApplicationScoped, wstrzyknij tę rzecz jako współpracownika (nie potrzebujesz nawet bałaganiarskich konstruktorów do testowania) i kontynuuj kpiny.


3
Stworzyłem narzędzie do automatycznego generowania interfejsów „mixin” Java 8, które zawijają wywołania statyczne: github.com/aro-tech/interface-it Wygenerowane mixiny można wyśmiewać jak każdy inny interfejs lub jeśli testowana klasa „implementuje” interfejs można zastąpić dowolną z jego metod w podklasie testu.
aro_tech

25

Miałem podobny problem. Przyjęta odpowiedź nie działała dla mnie, dopóki nie dokonałem zmiany: @PrepareForTest(TheClassThatContainsStaticMethod.class)zgodnie z dokumentacją PowerMock dla mockStatic .

I nie muszę używać BDDMockito .

Moja klasa:

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

Moja klasa testowa:

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

Nie mogę się zorientować? .MockStatic i? .W chwili obecnej z JUnit 4
Teddy

PowerMock.mockStatic & Mockito.when wydaje się nie działać.
Teddy

Dla każdego, kto zobaczy to później, musiałem wpisać PowerMockito.mockStatic (StaticClass.class);
myśliciel

Musisz dołączyć artefakt maven powermock-api-mockito.
PeterS

23

Jak wspomniano wcześniej, nie można kpić z metod statycznych za pomocą mockito.

Jeśli zmiana środowiska testowania nie jest opcją, możesz wykonać następujące czynności:

Utwórz interfejs dla DriverManager, wyśmiewaj ten interfejs, wstrzyknij go przez pewien rodzaj wstrzyknięcia zależności i sprawdź na tym wzorcu.


7

Uwaga: Kiedy wywołujesz metodę statyczną w jednostce statycznej, musisz zmienić klasę w @PrepareForTest.

Na przykład:

securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

Aby uzyskać powyższy kod, jeśli chcesz wyśmiewać klasę MessageDigest, użyj

@PrepareForTest(MessageDigest.class)

Chociaż jeśli masz coś takiego:

public class CustomObjectRule {

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

}

następnie musisz przygotować klasę, w której znajduje się ten kod.

@PrepareForTest(CustomObjectRule.class)

A następnie wykpisz metodę:

PowerMockito.mockStatic(MessageDigest.class);
PowerMockito.when(MessageDigest.getInstance(Mockito.anyString()))
      .thenThrow(new RuntimeException());

Waliłem głową w ścianę, próbując dowiedzieć się, dlaczego moja statyczna klasa nie kpiła. Można by pomyśleć, że we wszystkich samouczkach na interwebach ONE zajęłoby się czymś więcej niż tylko przypadkiem użycia.
SoftwareSavant,

6

Możesz to zrobić z odrobiną refaktoryzacji:

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

Następnie możesz rozszerzyć klasę, MySQLDatabaseConnectionFactoryaby zwracała próbne połączenie, robiła twierdzenia na temat parametrów itp.

Klasa rozszerzona może znajdować się w przypadku testowym, jeśli znajduje się w tym samym pakiecie (do czego zachęcam)

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


6

Mockito nie może przechwytywać metod statycznych, ale od Mockito 2.14.0 można go symulować, tworząc instancje metod statycznych.

Przykład (wyciąg z ich testów ):

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

Ich celem nie jest bezpośrednie wspieranie statycznego kpina, ale poprawa jego publicznych interfejsów API, aby inne biblioteki, takie jak Powermockito , nie musiały polegać na wewnętrznych interfejsach API ani bezpośrednio musiały duplikować kodu Mockito. ( źródło )

Oświadczenie: Zespół Mockito uważa, że ​​droga do piekła jest wybrukowana metodami statycznymi. Jednak zadaniem Mockito nie jest ochrona twojego kodu przed metodami statycznymi. Jeśli nie podoba ci się twój zespół robiąc sobie statyczne drwiny, przestań używać Powermockito w swojej organizacji. Mockito musi ewoluować jako zestaw narzędzi z opiniotwórczą wizją pisania testów Java (np. Nie kpij ze statystyki !!!). Jednak Mockito nie jest dogmatyczny. Nie chcemy blokować niezalecanych przypadków użycia, takich jak wyśmiewanie statyczne. To po prostu nie nasza praca.



1

Ponieważ ta metoda jest statyczna, ma już wszystko, czego potrzebujesz, aby ją wykorzystać, więc pokonuje cel drwiny. Szydzenie z metod statycznych jest uważane za złą praktykę.

Jeśli spróbujesz to zrobić, oznacza to, że jest coś nie tak ze sposobem, w jaki chcesz przeprowadzić testowanie.

Oczywiście możesz użyć PowerMockito lub dowolnego innego frameworka, który może to zrobić, ale spróbuj przemyśleć swoje podejście.

Na przykład: spróbuj wyśmiewać / dostarczyć obiekty, które zamiast tego zużywa ta metoda statyczna.


0

Użyj frameworka JMockit . To zadziałało dla mnie. Nie musisz pisać instrukcji dla kpiącej metody DBConenction.getConnection (). Wystarczy poniższy kod.

@Mock poniżej to pakiet 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;
            }
         };
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.