Mocking các biến thành viên của một lớp bằng Mockito


136

Tôi là một người mới phát triển và đặc biệt là các bài kiểm tra đơn vị. Tôi đoán yêu cầu của tôi là khá đơn giản, nhưng tôi rất muốn biết suy nghĩ của người khác về điều này.

Giả sử tôi có hai lớp như vậy -

public class First {

    Second second ;

    public First(){
        second = new Second();
    }

    public String doSecond(){
        return second.doSecond();
    }
}

class Second {

    public String doSecond(){
        return "Do Something";
    }
}

Giả sử tôi đang viết bài kiểm tra đơn vị để kiểm tra First.doSecond()phương pháp. Tuy nhiên, giả sử, tôi muốn Second.doSecond()lớp Mock như vậy. Tôi đang sử dụng Mockito để làm điều này.

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Tôi đang thấy rằng sự chế giễu không có hiệu lực và sự khẳng định thất bại. Có cách nào để giả định các biến thành viên của một lớp mà tôi muốn kiểm tra. ?

Câu trả lời:


86

Bạn cần cung cấp một cách truy cập các biến thành viên để bạn có thể chuyển qua một giả (các cách phổ biến nhất sẽ là phương thức setter hoặc hàm tạo có tham số).

Nếu mã của bạn không cung cấp cách thực hiện việc này, thì mã đó không chính xác cho TDD (Phát triển dựa trên thử nghiệm).


4
Cảm ơn. Tôi thấy nó. Tôi chỉ tự hỏi, làm thế nào tôi có thể thực hiện các thử nghiệm tích hợp bằng cách sử dụng giả, trong đó có thể có nhiều phương thức nội bộ, các lớp có thể cần phải được chế giễu, nhưng không nhất thiết phải có sẵn để được đặt qua setXXX () trước khi sử dụng.
Anand Hemmige

2
Sử dụng khung tiêm phụ thuộc, với cấu hình thử nghiệm. Vẽ sơ đồ trình tự của bài kiểm tra tích hợp mà bạn đang cố gắng thực hiện. Yếu tố sơ đồ trình tự vào các đối tượng mà bạn thực sự có thể kiểm soát. Điều này có nghĩa là nếu bạn đang làm việc với một lớp khung có mô hình chống đối tượng phụ thuộc mà bạn hiển thị ở trên, thì bạn nên coi đối tượng và thành viên có yếu tố đó là một đơn vị theo sơ đồ trình tự. Hãy chuẩn bị để điều chỉnh bao thanh toán của bất kỳ mã nào bạn kiểm soát, để làm cho nó dễ kiểm tra hơn.
kittylyst

9
Kính gửi @kittylyst, có lẽ nó sai từ quan điểm TDD hoặc từ bất kỳ quan điểm hợp lý nào. Nhưng đôi khi một nhà phát triển làm việc ở những nơi không có ý nghĩa gì cả và mục tiêu duy nhất mà người ta có chỉ là hoàn thành những câu chuyện bạn đã giao và đi. Vâng, đó là sai, nó không có ý nghĩa, những người không đủ điều kiện nhận các quyết định quan trọng và tất cả những điều đó. Vì vậy, vào cuối ngày, các mô hình chống chiến thắng rất nhiều.
amana

1
Tôi tò mò về điều này, nếu một thành viên trong lớp không có lý do gì để được thiết lập từ bên ngoài, tại sao chúng ta phải tạo ra một setter chỉ với mục đích thử nghiệm nó? Hãy tưởng tượng lớp 'Thứ hai' ở đây thực sự là một trình quản lý hoặc công cụ FileSystem, được khởi tạo trong quá trình xây dựng đối tượng để kiểm tra. Tôi có tất cả lý do để muốn giả định trình quản lý FileSystem này, để kiểm tra Lớp đầu tiên và không có lý do nào để làm cho nó có thể truy cập được. Tôi có thể làm điều này bằng Python, vậy tại sao không với Mockito?
Zangdar

65

Điều này là không thể nếu bạn không thể thay đổi mã của mình. Nhưng tôi thích tiêm phụ thuộc và Mockito hỗ trợ nó:

public class First {    
    @Resource
    Second second;

    public First() {
        second = new Second();
    }

    public String doSecond() {
        return second.doSecond();
    }
}

Bài kiểm tra của bạn:

@RunWith(MockitoJUnitRunner.class)
public class YourTest {
   @Mock
   Second second;

   @InjectMocks
   First first = new First();

   public void testFirst(){
      when(second.doSecond()).thenReturn("Stubbed Second");
      assertEquals("Stubbed Second", first.doSecond());
   }
}

Điều này là rất tốt đẹp và dễ dàng.


2
Tôi nghĩ rằng đây là một câu trả lời tốt hơn so với những người khác bởi vì MethMocks.
sudocoder

Thật buồn cười khi một người mới, như một người mới thử nghiệm như tôi, tin tưởng vào các thư viện và khung nhất định. Tôi đã giả định đây chỉ là một ý tưởng tồi cho thấy cần phải thiết kế lại ... cho đến khi bạn chỉ cho tôi nó thực sự tốt (rất rõ ràng và sạch) trong Mockito.
loài gặm nhấm mike

9
@ Nguồn là gì?
IgorGanapolsky

3
@IgorGanapolsky Tài nguyên @ là một chú thích được tạo / sử dụng bởi khung công tác Java Spring. Đây là một cách để chỉ ra cho Spring đây là một bean / object được quản lý bởi Spring. stackoverflow.com/questions/4093504/resource-vs-autowired baeldung.com/spring-annotations-resource-inject-autowire Nó không phải là một điều Mockito, nhưng vì nó được sử dụng trong lớp thử nghiệm phi nó phải được chế giễu trong kiểm tra.
Grez.Kev

Tôi không hiểu câu trả lời này. Bạn nói rằng nó không thể thì bạn cho thấy nó có thể chứ? Chính xác thì điều gì là không thể ở đây?
Goldname

35

Nếu bạn nhìn kỹ vào mã của mình, bạn sẽ thấy rằng secondtài sản trong bài kiểm tra của bạn vẫn là một ví dụ Second, không phải là một bản giả (bạn không chuyển bản giả chofirst mã của mình).

Cách đơn giản nhất là tạo ra một setter cho secondtrongFirst lớp và vượt qua nó một cách rõ ràng.

Như thế này:

public class First {

Second second ;

public First(){
    second = new Second();
}

public String doSecond(){
    return second.doSecond();
}

    public void setSecond(Second second) {
    this.second = second;
    }


}

class Second {

public String doSecond(){
    return "Do Something";
}
}

....

public void testFirst(){
Second sec = mock(Second.class);
when(sec.doSecond()).thenReturn("Stubbed Second");


First first = new First();
first.setSecond(sec)
assertEquals("Stubbed Second", first.doSecond());
}

Một cách khác là để vượt qua một Secondví dụ nhưFirst tham số hàm tạo.

Nếu bạn không thể sửa đổi mã, tôi nghĩ rằng tùy chọn duy nhất sẽ là sử dụng sự phản chiếu:

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");


    First first = new First();
    Field privateField = PrivateObject.class.
        getDeclaredField("second");

    privateField.setAccessible(true);

    privateField.set(first, sec);

    assertEquals("Stubbed Second", first.doSecond());
}

Nhưng bạn có thể có thể, vì hiếm khi thực hiện kiểm tra mã mà bạn không kiểm soát (mặc dù người ta có thể tưởng tượng ra một kịch bản mà bạn phải kiểm tra thư viện bên ngoài vì tác giả không làm như vậy :))


Hiểu rồi. Tôi có thể sẽ đi với đề nghị đầu tiên của bạn.
Anand Hemmige

Chỉ cần tò mò, có bất kỳ cách hoặc API nào bạn biết có thể giả định một đối tượng / phương thức ở cấp ứng dụng hoặc cấp gói. ? Tôi đoán những gì tôi đang nói là, trong ví dụ trên khi tôi chế nhạo đối tượng 'Thứ hai', có cách nào nó có thể ghi đè mọi phiên bản của Thứ hai được sử dụng trong suốt vòng đời thử nghiệm. ?
Anand Hemmige

@AnandHemmige thực sự là cái thứ hai (constructor) sạch hơn, vì nó tránh việc tạo ra các cá thể `Thứ hai. Các lớp học của bạn được tách riêng theo cách đó.
soulcheck

10
Mockito cung cấp một số chú thích đẹp để cho phép bạn đưa giả của mình vào các biến riêng tư. Chú thích Thứ hai với @Mockvà chú thích Đầu tiên với @InjectMocksvà khởi tạo Đầu tiên trong trình khởi tạo. Mockito sẽ tự động làm tốt nhất là tìm một nơi để đưa giả thứ hai vào ví dụ thứ nhất, bao gồm đặt các trường riêng phù hợp với loại.
jhericks

@Mocklà khoảng 1,5 (có thể sớm hơn, tôi không chắc chắn). 1.8.3 được giới thiệu @InjectMockscũng như @Spy@Captor.
jhericks

7

Nếu bạn không thể thay đổi biến thành viên, thì cách khác là sử dụng powerMockit và gọi

Second second = mock(Second.class)
when(second.doSecond()).thenReturn("Stubbed Second");
whenNew(Second.class).withAnyArguments.thenReturn(second);

Bây giờ vấn đề là BẤT K call cuộc gọi nào đến Thứ Hai mới sẽ trả về cùng một ví dụ bị chế giễu. Nhưng trong trường hợp đơn giản của bạn, điều này sẽ làm việc.


6

Tôi gặp vấn đề tương tự khi không đặt giá trị riêng tư vì Mockito không gọi siêu kiến ​​trúc sư. Đây là cách tôi tăng cường chế giễu với sự phản ánh.

Đầu tiên, tôi đã tạo một lớp TestUtils có chứa nhiều tiện ích hữu ích bao gồm các phương thức phản chiếu này. Truy cập phản ánh là một chút khó khăn để thực hiện mỗi lần. Tôi đã tạo ra các phương thức này để kiểm tra mã trên các dự án, vì lý do này hay lý do khác, không có gói chế nhạo và tôi không được mời đưa vào nó.

public class TestUtils {
    // get a static class value
    public static Object reflectValue(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(classToReflect, fieldNameValueToFetch);
            reflectField.setAccessible(true);
            Object reflectValue = reflectField.get(classToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // get an instance value
    public static Object reflectValue(Object objToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameValueToFetch);
            Object reflectValue = reflectField.get(objToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // find a field in the class tree
    public static Field reflectField(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField = null;
            Class<?> classForReflect = classToReflect;
            do {
                try {
                    reflectField = classForReflect.getDeclaredField(fieldNameValueToFetch);
                } catch (NoSuchFieldException e) {
                    classForReflect = classForReflect.getSuperclass();
                }
            } while (reflectField==null || classForReflect==null);
            reflectField.setAccessible(true);
            return reflectField;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch +" from "+ classToReflect);
        }
        return null;
    }
    // set a value with no setter
    public static void refectSetValue(Object objToReflect, String fieldNameToSet, Object valueToSet) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameToSet);
            reflectField.set(objToReflect, valueToSet);
        } catch (Exception e) {
            fail("Failed to reflectively set "+ fieldNameToSet +"="+ valueToSet);
        }
    }

}

Sau đó, tôi có thể kiểm tra lớp với một biến riêng tư như thế này. Điều này rất hữu ích để chế nhạo sâu trong các cây lớp mà bạn không kiểm soát được.

@Test
public void testWithRectiveMock() throws Exception {
    // mock the base class using Mockito
    ClassToMock mock = Mockito.mock(ClassToMock.class);
    TestUtils.refectSetValue(mock, "privateVariable", "newValue");
    // and this does not prevent normal mocking
    Mockito.when(mock.somthingElse()).thenReturn("anotherThing");
    // ... then do your asserts
}

Tôi đã sửa đổi mã của tôi từ dự án thực tế của tôi ở đây, trong trang. Có thể có một hoặc hai vấn đề biên dịch. Tôi nghĩ rằng bạn có được ý tưởng chung. Hãy lấy mã và sử dụng nó nếu bạn thấy nó hữu ích.


Bạn có thể giải thích mã của bạn với một usecase thực tế? như lớp công khai tobeMocker () {private ClassObject classObject; } Trong đó classObject bằng với đối tượng được thay thế.
Jasper Lankhorst

Trong ví dụ của bạn, nếu ToBeMocker dụ = new ToBeMocker (); và ClassObject someNewInstance = new ClassObject () {@Override // một cái gì đó giống như một phụ thuộc bên ngoài}; sau đó TestUtils.refelctSetValue (ví dụ: "classObject", someNewInstance); Lưu ý rằng bạn phải tìm ra những gì bạn muốn ghi đè để chế nhạo. Hãy nói rằng bạn có cơ sở dữ liệu và ghi đè này sẽ trả về một giá trị do đó bạn không cần phải chọn. Gần đây nhất tôi có một xe buýt dịch vụ mà tôi không thực sự muốn xử lý tin nhắn nhưng muốn đảm bảo nó nhận được nó. Vì vậy, tôi đặt trường hợp xe buýt riêng theo cách này - Hữu ích?
dave

Bạn sẽ phải tưởng tượng có định dạng trong bình luận đó. Nó đã được gỡ bỏ. Ngoài ra, điều này sẽ không hoạt động với Java 9 vì nó sẽ khóa truy cập riêng tư. Chúng tôi sẽ phải làm việc với một số cấu trúc khác sau khi chúng tôi có bản phát hành chính thức và có thể làm việc với các giới hạn thực tế của nó.
dave

1

Nhiều người khác đã khuyên bạn nên suy nghĩ lại về mã của mình để làm cho nó dễ kiểm chứng hơn - lời khuyên tốt và thường đơn giản hơn những gì tôi sắp đề xuất.

Nếu bạn không thể thay đổi mã để làm cho nó dễ kiểm tra hơn, PowerMock: https://code.google.com.vn/p/powermock/

PowerMock mở rộng Mockito (vì vậy bạn không phải học một khung giả mới), cung cấp chức năng bổ sung. Điều này bao gồm khả năng để một nhà xây dựng trả lại một giả. Mạnh mẽ, nhưng hơi phức tạp - vì vậy hãy sử dụng nó một cách thận trọng.

Bạn sử dụng một người chạy Mock khác. Và bạn cần chuẩn bị lớp sẽ gọi hàm tạo. (Lưu ý rằng đây là một gotcha thông thường - chuẩn bị lớp gọi hàm tạo, không phải lớp được xây dựng)

@RunWith(PowerMockRunner.class)
@PrepareForTest({First.class})

Sau đó, trong thiết lập thử nghiệm của bạn, bạn có thể sử dụng phương thức whenNew để có hàm tạo trả về một giả

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class));

0

Có, điều này có thể được thực hiện, như thử nghiệm sau đây cho thấy (được viết bằng API chế nhạo JMockit, mà tôi phát triển):

@Test
public void testFirst(@Mocked final Second sec) {
    new NonStrictExpectations() {{ sec.doSecond(); result = "Stubbed Second"; }};

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Tuy nhiên, với Mockito, một bài kiểm tra như vậy không thể được viết. Điều này là do cách thực hiện chế độ chế nhạo trong Mockito, trong đó một lớp con của lớp được chế giễu được tạo ra; chỉ các phiên bản của lớp con "giả" này có thể có hành vi giả, vì vậy bạn cần có mã được kiểm tra sử dụng chúng thay vì bất kỳ trường hợp nào khác.


3
câu hỏi không phải là liệu JMockit có tốt hơn Mockito hay không, mà là làm thế nào để làm điều đó trong Mockito. Hãy gắn bó với việc xây dựng một sản phẩm tốt hơn thay vì tìm kiếm cơ hội để làm hỏng cuộc thi!
TheZuck

8
Các poster ban đầu chỉ nói rằng anh ấy đang sử dụng Mockito; điều đó chỉ ngụ ý rằng Mockito là một yêu cầu cố định và khó khăn nên gợi ý rằng JMockit có thể xử lý tình huống này không phù hợp.
Bombe

0

Nếu bạn muốn một sự thay thế cho ReflectionTestUtils từ Spring trong mockito, hãy sử dụng

Whitebox.setInternalState(first, "second", sec);

Chào mừng bạn đến với Stack Overflow! Có những câu trả lời khác cung cấp câu hỏi của OP và chúng đã được đăng từ nhiều năm trước. Khi đăng câu trả lời, vui lòng đảm bảo bạn thêm một giải pháp mới hoặc giải thích rõ ràng hơn, đặc biệt là khi trả lời các câu hỏi cũ hơn hoặc nhận xét về các câu trả lời khác.
help-info.de
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.