Chế độ tĩnh với Mockito


373

Tôi đã viết một nhà máy để sản xuất java.sql.Connectioncác đối tượng:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

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

Tôi muốn xác thực các tham số được truyền cho DriverManager.getConnection, nhưng tôi không biết làm thế nào để giả định một phương thức tĩnh. Tôi đang sử dụng JUnit 4 và Mockito cho các trường hợp thử nghiệm của mình. Có cách nào tốt để giả / xác minh trường hợp sử dụng cụ thể này không?



5
Bạn không thể với mockito bằng cách desing :)
MariuszS

25
@MariuszS Không phải do thiết kế mà Mockito (hoặc EasyMock hoặc jMock) không hỗ trợ staticcác phương thức chế nhạo , mà là tình cờ . Giới hạn này (cùng với việc không hỗ trợ cho finalcác lớp / phương thức giả, hoặc newcác đối tượng -ed) là kết quả tự nhiên (nhưng ngoài ý muốn) của cách tiếp cận được sử dụng để thực hiện chế độ nhạo báng, trong đó các lớp mới được tạo động để thực hiện / mở rộng kiểu được chế giễu; các thư viện chế giễu khác sử dụng các phương pháp khác để tránh những hạn chế này. Điều này cũng xảy ra trong thế giới .NET.
Rogério

2
@ Rogério Cảm ơn bạn đã giải thích. github.com/mockito/mockito/wiki/FAQ Tôi có thể giả định các phương thức tĩnh không? Không. Mockito thích định hướng đối tượng và tiêm phụ thuộc vào mã thủ tục tĩnh, khó hiểu và thay đổi. Có một số thiết kế đằng sau giới hạn này quá :)
MariuszS

17
@MariuszS Tôi đọc rằng như một nỗ lực để loại bỏ các trường hợp sử dụng hợp pháp thay vì thừa nhận công cụ này có những hạn chế không thể (dễ dàng) loại bỏ và không cung cấp bất kỳ lý do hợp lý nào. BTW, đây là một cuộc thảo luận cho quan điểm ngược lại, với các tài liệu tham khảo.
Rogério

Câu trả lời:


350

Sử dụng PowerMockito trên đầu Mockito.

Mã ví dụ:

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

    }

Thêm thông tin:


4
Trong khi điều này hoạt động trên lý thuyết, có một thời gian khó khăn trong thực tế ...
Naftuli Kay

38
Thật không may, nhược điểm rất lớn của việc này là sự cần thiết của PowerMockRunner.
Innokenty

18
sut.execute ()? Có nghĩa?
TejjD

4
System Under Test, lớp yêu cầu giả lập DriverManager. kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs
MariuszS

8
FYI, nếu bạn đã sử dụng JUnit4, bạn có thể làm @RunWith(PowerMockRunner.class)và dưới mức đó @PowerMockRunnerDelegate(JUnit4.class).
EM-Creations

71

Chiến lược điển hình để tránh các phương thức tĩnh mà bạn không có cách nào tránh sử dụng, là bằng cách tạo các đối tượng được bọc và sử dụng các đối tượng trình bao bọc thay thế.

Các đối tượng trình bao bọc trở thành mặt tiền cho các lớp tĩnh thực sự và bạn không kiểm tra chúng.

Một đối tượng bao bọc có thể là một cái gì đó như

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

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

Cuối cùng, lớp của bạn đang được kiểm tra có thể sử dụng đối tượng singleton này bằng cách, ví dụ, có một hàm tạo mặc định để sử dụng thực tế:

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

Và ở đây bạn có một lớp có thể dễ dàng được kiểm tra, bởi vì bạn không trực tiếp sử dụng một lớp với các phương thức tĩnh.

Nếu bạn đang sử dụng CDI và có thể sử dụng chú thích @Inject thì điều đó thậm chí còn dễ dàng hơn. Chỉ cần làm cho Wrapper bean của bạn @ApplicationScoped, lấy thứ đó làm cộng tác viên (bạn thậm chí không cần các nhà xây dựng lộn xộn để thử nghiệm) và tiếp tục chế giễu.


3
Tôi đã tạo một công cụ để tự động tạo các giao diện "mixin" Java 8, bao gồm các lệnh gọi tĩnh: github.com/aro-tech/interface-it Các mixin được tạo có thể được mô phỏng như bất kỳ giao diện nào khác hoặc nếu lớp của bạn được kiểm tra "thực hiện" giao diện bạn có thể ghi đè bất kỳ phương thức nào của nó trong một lớp con để kiểm tra.
aro_tech

25

Tôi đã có một vấn đề tương tự. Câu trả lời được chấp nhận không có tác dụng với tôi, cho đến khi tôi thực hiện thay đổi : @PrepareForTest(TheClassThatContainsStaticMethod.class), theo tài liệu của PowerMock cho mockStatic .

Và tôi không phải sử dụng BDDMockito.

Lớp học của tôi:

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

Lớp kiểm tra của tôi:

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

Không thể tìm ra? .MockStatic và ?. Hiện tại với JUnit 4
Teddy

PowerMock.mockStatic & Mockito.when dường như không hoạt động.
Teddy

Đối với bất kỳ ai nhìn thấy điều này sau này, đối với tôi, tôi đã phải nhập PowerMockito.mockStatic (StaticClass. Class);
suy nghĩ

Bạn cần bao gồm động cơ powermock-api-mockito maven.
PeterS

23

Như đã đề cập trước đây, bạn không thể mô phỏng các phương thức tĩnh với mockito.

Nếu thay đổi khung thử nghiệm của bạn không phải là một tùy chọn, bạn có thể làm như sau:

Tạo một giao diện cho DriverManager, giả định giao diện này, tiêm nó thông qua một số kiểu tiêm phụ thuộc và xác minh trên giả đó.


7

Quan sát: Khi bạn gọi phương thức tĩnh trong một thực thể tĩnh, bạn cần thay đổi lớp trong @PrepareForTest.

Ví dụ:

securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

Đối với mã trên nếu bạn cần mô phỏng lớp MessageDigest, hãy sử dụng

@PrepareForTest(MessageDigest.class)

Trong khi nếu bạn có một cái gì đó như dưới đây:

public class CustomObjectRule {

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

}

sau đó, bạn cần chuẩn bị lớp mã này nằm trong đó.

@PrepareForTest(CustomObjectRule.class)

Và sau đó chế nhạo phương thức:

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

Tôi đập đầu vào tường cố gắng tìm ra lý do tại sao lớp tĩnh của tôi không chế nhạo. Bạn sẽ nghĩ trong tất cả các hướng dẫn về các interwebs, ONE sẽ đi sâu hơn trường hợp sử dụng xương trần.
Phần

6

Bạn có thể làm điều đó với một chút tái cấu trúc:

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

Sau đó, bạn có thể mở rộng lớp của mình MySQLDatabaseConnectionFactoryđể trả về một kết nối bị chế giễu, thực hiện các xác nhận về các tham số, v.v.

Lớp mở rộng có thể nằm trong trường hợp thử nghiệm, nếu nó nằm trong cùng một gói (mà tôi khuyến khích bạn làm)

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

Để giả định phương thức tĩnh, bạn nên sử dụng Powermock, xem tại: https://github.com/powermock/powermock/wiki/MockStatic . Mockito không cung cấp chức năng này.

Bạn có thể đọc một bài viết hay về mockito: http://refcardz.dzone.com/refcardz/mockito


2
Vui lòng không liên kết đến một trang web. Câu trả lời nên bao gồm câu trả lời có thể sử dụng thực tế. Nếu trang web bị sập hoặc thay đổi, câu trả lời không còn giá trị.
the_new_mr

6

Mockito không thể chụp các phương thức tĩnh, nhưng vì Mockito 2.14.0, bạn có thể mô phỏng nó bằng cách tạo các thể hiện gọi của các phương thức tĩnh.

Ví dụ (trích từ các thử nghiệm của họ ):

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

Mục tiêu của họ không phải là trực tiếp hỗ trợ chế độ tĩnh, mà là cải thiện các API công khai để các thư viện khác, như Powermockito , không phải dựa vào API nội bộ hoặc trực tiếp phải sao chép một số mã Mockito. ( nguồn )

Tuyên bố miễn trừ trách nhiệm: Nhóm Mockito nghĩ rằng con đường đến địa ngục được lát bằng các phương pháp tĩnh. Tuy nhiên, công việc của Mockito không phải là bảo vệ mã của bạn khỏi các phương thức tĩnh. Nếu bạn không thích nhóm của mình thực hiện chế độ tĩnh, hãy ngừng sử dụng Powermockito trong tổ chức của bạn. Mockito cần phát triển như một bộ công cụ với tầm nhìn đầy quan điểm về cách viết các bài kiểm tra Java (ví dụ: không giả định thống kê !!!). Tuy nhiên, Mockito không giáo điều. Chúng tôi không muốn chặn các trường hợp sử dụng không được khuyến nghị như chế độ tĩnh. Đó không phải là công việc của chúng tôi.



1

Vì phương thức đó là tĩnh, nên nó đã có mọi thứ bạn cần để sử dụng, vì vậy nó đánh bại mục đích chế giễu. Mocking các phương thức tĩnh được coi là một thực hành xấu.

Nếu bạn cố gắng làm điều đó, điều đó có nghĩa là có điều gì đó không đúng với cách bạn muốn thực hiện kiểm tra.

Tất nhiên bạn có thể sử dụng PowerMockito hoặc bất kỳ khung nào khác có khả năng làm điều đó, nhưng hãy thử suy nghĩ lại về cách tiếp cận của bạn.

Ví dụ: cố gắng giả định / cung cấp các đối tượng mà phương thức tĩnh đó tiêu thụ thay thế.


0

Sử dụng khung JMockit . Nó làm việc cho tôi. Bạn không phải viết các câu lệnh để chế nhạo phương thức DBConenction.getConnection (). Chỉ cần mã dưới đây là đủ.

@Mock bên dưới là gói 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;
            }
         };
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.