Mengejek metode statis dengan Mockito

374

Saya telah menulis sebuah pabrik untuk menghasilkan java.sql.Connectionobjek:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

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

Saya ingin memvalidasi parameter yang diteruskan ke DriverManager.getConnection, tetapi saya tidak tahu bagaimana cara mengejek metode statis. Saya menggunakan JUnit 4 dan Mockito untuk test case saya. Apakah ada cara yang baik untuk mengejek / memverifikasi kasus penggunaan khusus ini?

Naftuli Kay
sumber
1
akankah ini membantu? stackoverflow.com/questions/19464975/…
sasankad
5
Anda tidak dapat dengan mockito dengan desing :)
MariuszS
25
@MariuszS Bukan karena desain bahwa Mockito (atau EasyMock, atau jMock) tidak mendukung staticmetode mengejek , tetapi secara tidak sengaja . Batasan ini (bersama dengan tidak ada dukungan untuk finalkelas / metode mengejek , atau newobjek -ed) adalah konsekuensi alami (tetapi tidak disengaja) dari pendekatan yang digunakan untuk mengimplementasikan mengejek, di mana kelas baru secara dinamis dibuat yang mengimplementasikan / memperluas jenis yang akan diejek; perpustakaan mengejek lainnya menggunakan pendekatan lain yang menghindari keterbatasan ini. Ini terjadi di dunia .NET juga.
Rogério
2
@ Rogério Terima kasih atas penjelasannya. github.com/mockito/mockito/wiki/FAQ Dapatkah saya mengejek metode statis? Tidak. Mockito lebih memilih orientasi objek dan injeksi ketergantungan daripada kode prosedural statis yang sulit dimengerti & diubah. Ada beberapa desain di balik batasan ini juga :)
MariuszS
17
@MariuszS Saya membaca bahwa sebagai upaya untuk memberhentikan kasus penggunaan yang sah alih-alih mengakui alat ini memiliki keterbatasan yang tidak dapat (mudah) dihapus, dan tanpa memberikan alasan yang beralasan. BTW, berikut adalah diskusi untuk sudut pandang yang berlawanan, dengan referensi.
Rogério 3-15

Jawaban:

350

Gunakan PowerMockito di atas Mockito.

Kode contoh:

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

    }

Informasi lebih lanjut:

MariuszS
sumber
4
Sementara ini bekerja secara teori, mengalami kesulitan dalam praktek ...
Naftuli Kay
38
Sayangnya kerugian besar dari ini adalah kebutuhan untuk PowerMockRunner.
Innokenty
18
sut.execute ()? Cara?
TejjD
4
System Under Test, kelas yang membutuhkan tiruan dari DriverManager. kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs
MariuszS
8
FYI, jika Anda sudah menggunakan JUnit4 Anda dapat melakukan @RunWith(PowerMockRunner.class)dan di bawah itu @PowerMockRunnerDelegate(JUnit4.class).
EM-Creations
71

Strategi khas untuk menghindari metode statis yang tidak bisa Anda hindari, adalah dengan membuat objek terbungkus dan menggunakan objek pembungkus.

Objek pembungkus menjadi fasad ke kelas statis nyata, dan Anda tidak menguji mereka.

Objek pembungkus bisa berupa sesuatu

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

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

Akhirnya, kelas Anda yang diuji dapat menggunakan objek tunggal ini dengan, misalnya, memiliki konstruktor default untuk penggunaan kehidupan nyata:

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

Dan di sini Anda memiliki kelas yang dapat dengan mudah diuji, karena Anda tidak langsung menggunakan kelas dengan metode statis.

Jika Anda menggunakan CDI dan dapat menggunakan anotasi @Inject maka lebih mudah. Cukup buat Wrapper bean Anda @ApplicationScoped, dapatkan benda itu disuntikkan sebagai kolaborator (Anda bahkan tidak perlu konstruktor yang berantakan untuk pengujian), dan teruskan mengejeknya.

99Sono
sumber
3
Saya membuat alat untuk secara otomatis menghasilkan antarmuka Java 8 "mixin" yang membungkus panggilan statis: github.com/aro-tech/interface-it Mixin yang dihasilkan dapat diejek seperti antarmuka lainnya, atau jika kelas Anda yang diuji "mengimplementasikan" antarmuka Anda dapat mengesampingkan salah satu metode dalam subkelas untuk pengujian.
aro_tech
25

Saya punya masalah serupa. Jawaban yang diterima tidak bekerja untuk saya, sampai saya membuat perubahan :,@PrepareForTest(TheClassThatContainsStaticMethod.class) menurut dokumentasi PowerMock untuk mockStatic .

Dan saya tidak harus menggunakan BDDMockito.

Kelasku:

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

Kelas ujian saya:

@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
sumber
Tidak dapat menemukan? .MockStatic dan?. Saat ini dengan JUnit 4
Teddy
PowerMock.mockStatic & Mockito.when sepertinya tidak berfungsi.
Teddy
Bagi siapa pun yang melihat ini nanti, bagi saya saya harus mengetik PowerMockito.mockStatic (StaticClass.class);
pemikir
Anda harus menyertakan arterfact powermock-api-mockito maven.
PeterS
23

Seperti yang disebutkan sebelumnya Anda tidak dapat mengejek metode statis dengan mockito.

Jika mengubah kerangka kerja pengujian Anda bukan opsi, Anda dapat melakukan hal berikut:

Buat antarmuka untuk DriverManager, tiru antarmuka ini, suntikkan melalui semacam injeksi ketergantungan dan verifikasi pada tiruan itu.

ChrisM
sumber
7

Pengamatan: Ketika Anda memanggil metode statis dalam entitas statis, Anda perlu mengubah kelas di @PrepareForTest.

Untuk misalnya:

securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

Untuk kode di atas jika Anda perlu mengejek kelas MessageDigest, gunakan

@PrepareForTest(MessageDigest.class)

Sedangkan jika Anda memiliki sesuatu seperti di bawah ini:

public class CustomObjectRule {

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

}

lalu, Anda harus menyiapkan kelas tempat kode ini berada.

@PrepareForTest(CustomObjectRule.class)

Dan kemudian mengejek metode ini:

PowerMockito.mockStatic(MessageDigest.class);
PowerMockito.when(MessageDigest.getInstance(Mockito.anyString()))
      .thenThrow(new RuntimeException());
beberapa pria acak
sumber
Aku membenturkan kepalaku ke dinding mencoba mencari tahu mengapa kelas statisku tidak mengejek. Anda akan berpikir di semua tutorial tentang jalinan, SATU akan pergi ke lebih dari kasus penggunaan telanjang-tulang.
SoftwareSavant
6

Anda dapat melakukannya dengan sedikit 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...);
    }
}

Kemudian Anda dapat memperluas kelas Anda MySQLDatabaseConnectionFactoryuntuk mengembalikan koneksi yang diejek, melakukan pernyataan pada parameter, dll.

Kelas yang diperluas dapat berada di dalam test case, jika terletak di paket yang sama (yang saya sarankan Anda lakukan)

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

Mockito tidak dapat menangkap metode statis, tetapi karena Mockito 2.14.0 Anda dapat mensimulasikannya dengan membuat contoh doa metode statis.

Contoh (diekstraksi dari tes mereka ):

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

Tujuan mereka bukan untuk secara langsung mendukung penghinaan statis, tetapi untuk meningkatkan API publiknya sehingga perpustakaan lain, seperti Powermockito , tidak harus bergantung pada API internal atau secara langsung harus menduplikasi beberapa kode Mockito. ( sumber )

Penafian: Tim Mockito berpikir bahwa jalan menuju neraka ditaburi dengan metode statis. Namun, tugas Mockito bukan untuk melindungi kode Anda dari metode statis. Jika Anda tidak suka tim Anda mengejek statis, berhentilah menggunakan Powermockito di organisasi Anda. Mockito perlu berevolusi sebagai toolkit dengan visi berpendapat tentang bagaimana tes Java harus ditulis (misalnya jangan mengejek statika !!!). Namun, Mockito tidak dogmatis. Kami tidak ingin memblokir kasus penggunaan yang tidak direkomendasikan seperti mengejek statis. Itu bukan tugas kita.

David Miguel
sumber
1

Karena metode itu statis, ia sudah memiliki semua yang Anda perlukan untuk menggunakannya, sehingga ia mengalahkan tujuan mengejek. Mengejek metode statis dianggap sebagai praktik yang buruk.

Jika Anda mencoba melakukan itu, itu berarti ada sesuatu yang salah dengan cara Anda ingin melakukan pengujian.

Tentu saja Anda dapat menggunakan PowerMockito atau kerangka kerja lain yang mampu melakukan itu, tetapi cobalah untuk memikirkan kembali pendekatan Anda.

Sebagai contoh: cobalah untuk mengejek / menyediakan objek, yang dikonsumsi oleh metode statis itu.

Benas
sumber
0

Gunakan kerangka kerja JMockit . Ini berhasil untuk saya. Anda tidak perlu menulis pernyataan untuk mengejek metode DBConenction.getConnection (). Hanya kode di bawah ini sudah cukup.

@Mock di bawah ini adalah 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;
            }
         };
Zlatan
sumber