Mockito ile statik yöntemleri alay etme


372

java.sql.ConnectionNesneleri üretmek için bir fabrika yazdım :

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

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

Geçirilen parametreleri doğrulamak istiyorum DriverManager.getConnection, ancak statik bir yöntemle alay etmeyi bilmiyorum. Test durumlarım için JUnit 4 ve Mockito kullanıyorum. Bu özel kullanım durumunu taklit etmenin / doğrulamanın iyi bir yolu var mı?



5
Mockito ile desing :) 'i
yapamazsın

25
@MariuszS Mockito'nun (veya EasyMock'un veya jMock'un) alay staticyöntemlerini desteklemediği değil , kazayla tasarladığı anlamına gelir . Bu sınırlama (alaycı finalsınıflar / yöntemler veya new-ed nesneler için destek olmadan ), alaycılığı uygulamak için kullanılan yaklaşımın doğal (ancak istenmeyen) bir sonucudur; burada alay edilecek türü uygulayan / genişleten dinamik olarak yeni sınıflar oluşturulur; diğer alay kütüphaneleri bu sınırlamalardan kaçınan başka yaklaşımlar kullanır. Bu .NET dünyasında da oldu.
Rogério

2
@ Rogério Açıklama için teşekkürler. github.com/mockito/mockito/wiki/FAQ Statik yöntemleri alay edebilir miyim? Hayır. Mockito, anlaşılması ve değiştirilmesi zor olan statik, prosedürel kod yerine nesne yönelimi ve bağımlılık enjeksiyonunu tercih eder. Bu sınırlamanın arkasında da bazı tasarımlar var :)
MariuszS

17
@MariuszS Aracı kabul etmek yerine meşru kullanım durumlarını reddetme çabası olarak (kolayca) kaldırılamayan ve herhangi bir gerekçe göstermeden sınırlamalar olduğunu okudum. BTW, karşıt bakış açısı için referanslarla böyle bir tartışma .
Rogério

Yanıtlar:


350

Mockito'nun üstünde PowerMockito kullanın .

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

    }

Daha fazla bilgi:


4
Bu teoride çalışırken , uygulamada zor zamanlar ...
Naftuli Kay

38
Ne yazık ki, bunun büyük dezavantajı PowerMockRunner'a ihtiyaçtır.
Innokenty

18
sut.execute ()? Anlamına geliyor?
TejjD

4
System Test altında, DriverManager'ın taklit edilmesini gerektiren sınıf. kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs
MariuszS

8
Zaten JUnit4 kullanıyorsanız Bilginize, yapabileceğiniz @RunWith(PowerMockRunner.class)ve bunun altında @PowerMockRunnerDelegate(JUnit4.class).
EM-Creations

71

Kullanmaktan kaçınamayacağınız statik yöntemlerden kaçmak için tipik strateji, sarılmış nesneler oluşturmak ve bunun yerine sarıcı nesneleri kullanmaktır.

Sarıcı nesneleri gerçek statik sınıfların cephelerine dönüşür ve bunları test etmezsiniz.

Bir sarıcı nesne,

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

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

Son olarak, test altındaki sınıfınız, örneğin, gerçek yaşam kullanımı için varsayılan bir kurucuya sahip olarak bu singleton nesnesini kullanabilir:

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

Ve burada kolayca test edilebilen bir sınıfınız var, çünkü bir sınıfı doğrudan statik yöntemlerle kullanmıyorsunuz.

CDI kullanıyorsanız ve @Inject ek açıklamasını kullanabiliyorsanız, bu daha da kolaylaşır. Sadece Wrapper bean'ınızı @ApplicationScoped yapın, bir şeyi ortak çalışan olarak enjekte edin (test için dağınık kuruculara bile ihtiyacınız yok) ve alay etmeye devam edin.


3
Statik çağrıları saran Java 8 "mixin" arayüzlerini otomatik olarak oluşturmak için bir araç oluşturdum: github.com/aro-tech/interface-it Oluşturulan mixins başka bir arayüz gibi alay edilebilir veya test altındaki sınıfınız " arabirimi, test için bir alt sınıftaki yöntemlerinden herhangi birini geçersiz kılabilirsiniz.
aro_tech

25

Benzer bir sorunum vardı. Değişikliği yapana kadar kabul edilen cevap benim için çalışmadı: PowerMock'un mockStatic belgelerine@PrepareForTest(TheClassThatContainsStaticMethod.class) göre .

Ve kullanmak zorunda değilim BDDMockito.

Sınıfım:

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

Test sınıfım:

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

? .MockStatic ve?. Şu anda JUnit 4
Teddy ile

PowerMock.mockStatic & Mockito.when çalışmıyor gibi görünüyor.
Teddy

Bunu daha sonra gören herkes için, benim için PowerMockito.mockStatic (StaticClass.class);
thinkereer

Powermock-api-mockito maven arterfactını eklemeniz gerekir.
PeterS

23

Daha önce de belirtildiği gibi, mockito ile statik yöntemleri alay edemezsiniz.

Test çerçevenizi değiştirmek bir seçenek değilse aşağıdakileri yapabilirsiniz:

DriverManager için bir arayüz oluşturun, bu arayüzü alay edin, bir çeşit bağımlılık enjeksiyonu yoluyla enjekte edin ve o alayda doğrulayın.


7

Gözlem: Statik bir varlık içindeki statik yöntemi çağırdığınızda, @PrepareForTest içindeki sınıfı değiştirmeniz gerekir.

Örneğin:

securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

Yukarıdaki kod için MessageDigest sınıfını taklit etmeniz gerekiyorsa,

@PrepareForTest(MessageDigest.class)

Eğer aşağıdaki gibi bir şey varsa:

public class CustomObjectRule {

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

}

o zaman bu kodun bulunduğu sınıfı hazırlamanız gerekir.

@PrepareForTest(CustomObjectRule.class)

Ve sonra yöntemi alay et:

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

Statik sınıfımın neden alay etmediğini anlamaya çalışırken başımı duvara vuruyordum. İnterweb'lerdeki tüm derslerde, ONE çıplak kemik kullanım durumundan daha fazlasına girerdi.
SoftwareSavant

6

Biraz yeniden düzenleme ile yapabilirsiniz:

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

Daha sonra MySQLDatabaseConnectionFactory, alaycı bir bağlantıyı döndürmek, parametreler hakkında iddialarda bulunmak vb . İçin sınıfınızı genişletebilirsiniz .

Genişletilmiş paket, aynı pakette bulunuyorsa (bunu yapmanızı öneririm) test senaryosunda bulunabilir.

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 statik yöntemleri yakalayamaz, ancak Mockito 2.14.0'dan beri statik yöntemlerin çağırma örneklerini oluşturarak simüle edebilirsiniz.

Örnek ( testlerinden çıkarılmıştır ):

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

Amaçları statik alaycılığı doğrudan desteklememek değil, genel API'lerini geliştirmek, böylece Powermockito gibi diğer kütüphanelerin dahili API'lere güvenmesi veya doğrudan bazı Mockito kodlarını kopyalaması gerekmiyor. ( kaynak )

Feragatname: Mockito ekibi cehenneme giden yolun statik yöntemlerle döşendiğini düşünüyor. Ancak, Mockito'nun işi kodunuzu statik yöntemlerden korumak değildir. Ekibinizin statik alay yapmasını sevmiyorsanız, kuruluşunuzda Powermockito kullanmayı bırakın. Mockito'nun Java testlerinin nasıl yazılması gerektiğine dair görüşlü bir vizyona sahip bir araç seti olarak gelişmesi gerekir (örn. Statik bilgileri alay etme !!!). Ancak Mockito dogmatik değildir. Statik alay gibi önerilmeyen kullanım durumlarını engellemek istemiyoruz. Bu sadece bizim işimiz değil.



1

Bu yöntem statik olduğundan, zaten kullanmak için ihtiyacınız olan her şeye sahiptir, bu yüzden alay etme amacını yener. Statik yöntemlerle alay etmek kötü bir uygulama olarak kabul edilir.

Bunu yapmaya çalışırsanız, test yapmak istediğiniz yolla ilgili bir sorun var demektir.

Elbette PowerMockito'yu veya bunu yapabilen başka bir çerçeveyi kullanabilirsiniz, ancak yaklaşımınızı yeniden düşünmeye çalışın.

Örneğin: bunun yerine statik yöntemin kullandığı nesneleri taklit etmeye / sağlamaya çalışın.


0

JMockit çerçevesini kullanın . Benim için çalıştı. Alay etmek için DBConenction.getConnection () yöntemi ifadeleri yazmak zorunda değilsiniz. Sadece aşağıdaki kod yeterlidir.

@Mock aşağıdaki mockit.Mock paketi

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;
            }
         };
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.