Mockito, JUnit và Spring


77

Tôi mới bắt đầu tìm hiểu về Mockito hôm nay. Tôi đã viết một số thử nghiệm đơn giản (với JUnit, xem bên dưới), nhưng tôi không thể tìm ra cách tôi có thể sử dụng đối tượng giả bên trong các bean được quản lý của Spring. Các phương pháp hay nhất để làm việc với Spring là gì. Làm thế nào tôi nên tiêm sự phụ thuộc chế nhạo vào bean của tôi?

Bạn có thể bỏ qua điều này cho đến khi quay lại câu hỏi của tôi .

Trước hết, những gì tôi đã học được. Đây là bài viết rất hay Mocks Aren't Stubs giải thích những điều cơ bản (Mock's kiểm tra xác minh hành vi không phải xác minh trạng thái ). Sau đó, có một ví dụ điển hình ở đây Mockito và ở đây Chế giễu dễ dàng hơn với mockito . Chúng tôi có lời giải thích rằng các đối tượng giả của Mockito đều là giảsơ khai .

Tại đây Mockito và đây Matchers , bạn có thể tìm thêm các ví dụ khác.

Bài kiểm tra này

@Test
public void testReal(){
    List<String> mockedList = mock(List.class);
     //stubbing
     //when(mockedList.get(0)).thenReturn("first");

    mockedList.get(anyInt());
    OngoingStubbing<String> stub= when(null);
    stub.thenReturn("first");

    //String res = mockedList.get(0);
                //System.out.println(res);

     //you can also verify using argument matcher
     //verify(mockedList).get(anyInt());

    verify(mockedList);
    mockedList.get(anyInt());
}

hoạt động tốt.

Trở lại câu hỏi của tôi. Ở đây Tiêm Mockito mock vào một Spring bean ai đó cố gắng sử dụng Springs ReflectionTestUtils.setField(), nhưng sau đó ở đây Spring Integration Tests, Tạo Mock Object, chúng tôi khuyên bạn nên thay đổi ngữ cảnh của Spring.

Tôi thực sự không hiểu hai liên kết cuối cùng ... Ai đó có thể giải thích cho tôi vấn đề Spring có gì với Mockito không? Có gì sai với giải pháp này?

@InjectMocks
private MyTestObject testObject

@Mock
private MyDependentObject mockedObject

@Before
public void setup() {
        MockitoAnnotations.initMocks(this);
}

https://stackoverflow.com/a/8742745/1137529

CHỈNH SỬA : Tôi không thực sự rõ ràng. Tôi sẽ cung cấp 3 ví dụ về mã để làm rõ bản thân của tôi: Giả sử, chúng ta có bean HelloWorld với phương thức printHello()và bean HelloFacade với phương thức sayHellochuyển tiếp các cuộc gọi đến phương thức của HelloWorld printHello().

Ví dụ đầu tiên là sử dụng ngữ cảnh của Spring và không có trình chạy tùy chỉnh, sử dụng ReflectionTestUtils để tiêm phụ thuộc (DI):

public class Hello1Test  {
private ApplicationContext ctx;

@Before
public void setUp() {
    MockitoAnnotations.initMocks(this);
    this.ctx = new ClassPathXmlApplicationContext("META-INF/spring/ServicesImplContext.xml");
}



@Test
public void testHelloFacade() {
    HelloFacade obj = (HelloFacade) ctx.getBean(HelloFacadeImpl.class);
    HelloWorld mock = mock(HelloWorld.class);
    doNothing().when(mock).printHello();

    ReflectionTestUtils.setField(obj, "hello", mock);
    obj.sayHello();

    verify(mock, times(1)).printHello();
}

}

Như @Noam đã chỉ ra có cách để chạy nó bằng cách gọi rõ ràng tới MockitoAnnotations.initMocks(this);. Tôi cũng sẽ bỏ sử dụng bối cảnh của Spring trong ví dụ này.

@RunWith(MockitoJUnitRunner.class)
public class Hello1aTest {


@InjectMocks
private HelloFacade obj =  new HelloFacadeImpl();

@Mock
private HelloWorld mock;


@Test
public void testHelloFacade() {
    doNothing().when(mock).printHello();
    obj.sayHello();
}

}

Một cách khác để làm điều này

public class Hello1aTest {

@Before
public void setUp() {
    MockitoAnnotations.initMocks(this);
}


@InjectMocks
private HelloFacadeImpl obj;

@Mock
private HelloWorld mock;


@Test
public void testHelloFacade() {
    doNothing().when(mock).printHello();
    obj.sayHello();
}

}

Không, trong ví dụ trước, chúng tôi phải cài đặt thủ công HelloFacadeImpl và gán nó cho HelloFacade, vì HelloFacade là giao diện. Trong ví dụ cuối cùng, chúng ta chỉ có thể khai báo HelloFacadeImpl và Mokito sẽ khởi tạo nó cho chúng ta. Hạn chế của phương pháp này là hiện nay, unit-under-test là impl-class chứ không phải interface.


1
gì đó sai với giải pháp? Bài đăng blog mà bạn liên kết đến không sử dụng @InjectMocks(tương đối gần đây, mặc dù IIRC trước bài đăng trên blog đó) nên có những trường hợp cần sắp xếp lại các định nghĩa bean có thể là cần thiết. Tôi không chắc câu hỏi rốt cuộc là gì.
Dave Newton

Mùa xuân không có vấn đề gì với Mockito. Hoặc ngược lại.
Rob Kielty

Tôi tin rằng trong hầu hết các trường hợp, bạn NÊN kiểm tra việc triển khai thực tế thay vì giao diện.
Adrian Shum

Tôi đã mở câu hỏi mới về vấn đề này stackoverflow.com/questions/10937763/…
alexsmail

Câu trả lời:


55

Thành thật mà nói, tôi không chắc liệu tôi có thực sự hiểu câu hỏi của bạn hay không: PI sẽ cố gắng làm rõ nhiều nhất có thể, từ những gì tôi nhận được từ câu hỏi ban đầu của bạn:

Đầu tiên, trong hầu hết các trường hợp, bạn KHÔNG nên lo lắng về mùa Xuân. Bạn hiếm khi cần đến mùa xuân để viết bài kiểm tra đơn vị của mình. Trong trường hợp bình thường, bạn chỉ cần khởi tạo hệ thống đang được kiểm tra (SUT, mục tiêu cần kiểm tra) trong bài kiểm tra đơn vị của bạn và cũng đưa các phụ thuộc của SUT vào kiểm tra. Các phụ thuộc thường là một mô hình / sơ khai.

Cách đề xuất ban đầu của bạn và ví dụ 2, 3 đang thực hiện chính xác những gì tôi đang mô tả ở trên.

Trong một số trường hợp hiếm hoi (như kiểm tra tích hợp hoặc một số kiểm tra đơn vị đặc biệt), bạn cần tạo ngữ cảnh ứng dụng Spring và lấy SUT của bạn từ ngữ cảnh ứng dụng. Trong trường hợp như vậy, tôi tin rằng bạn có thể:

1) Tạo SUT của bạn trong ứng dụng mùa xuân ctx, tham chiếu đến nó và đưa các mô phỏng vào nó

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("test-app-ctx.xml")
public class FooTest {

    @Autowired
    @InjectMocks
    TestTarget sut;

    @Mock
    Foo mockFoo;

    @Before
    /* Initialized mocks */
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void someTest() {
         // ....
    }
}

hoặc là

2) làm theo cách được mô tả trong liên kết của bạn Kiểm tra tích hợp mùa xuân, Tạo đối tượng giả . Cách tiếp cận này là tạo mô hình giả trong ngữ cảnh ứng dụng của Spring và bạn có thể lấy đối tượng mô phỏng từ ứng dụng ctx để thực hiện việc xác minh / khai báo của bạn:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("test-app-ctx.xml")
public class FooTest {

    @Autowired
    TestTarget sut;

    @Autowired
    Foo mockFoo;

    @Test
    public void someTest() {
         // ....
    }
}

Cả hai cách đều hoạt động. Sự khác biệt chính là trường hợp trước sẽ có các phụ thuộc được tiêm vào sau khi trải qua vòng đời của mùa xuân, v.v. (ví dụ: khởi tạo bean), trong khi trường hợp sau được tiêm trước các phần tử. Ví dụ: nếu SUT của bạn triển khai InitializingBean của mùa xuân và quy trình khởi tạo liên quan đến các phụ thuộc, bạn sẽ thấy sự khác biệt giữa hai cách tiếp cận này. Tôi tin rằng không có đúng hay sai cho 2 cách tiếp cận này, miễn là bạn biết bạn đang làm gì.

Chỉ là một phần bổ sung, @Mock, @Inject, MocktoJunitRunner, v.v. đều không cần thiết khi sử dụng Mockito. Chúng chỉ là những tiện ích giúp bạn tiết kiệm việc nhập Mockito.mock (Foo.class) và một loạt các lệnh gọi setter.


@Noam, hãy xem asnwer này. Adrian, câu trả lời tuyệt vời cảm ơn bạn. :-)
alexsmail

2
Tôi không nghĩ 1) hoạt động. Đó là: khi sử dụng cả hai @Autowired@InjectMocks, tôi thấy hạt đậu được tiêm Spring, không phải hạt giẻ, vào TestTarget. (Tôi đã hy vọng rằng việc sử dụng cả hai chú thích sẽ chỉ đưa vào một mô hình cho Foo, nhưng vẫn sử dụng các bean được tiêm Spring mặc định cho tất cả các phụ thuộc khác được tự động nối dây TestTargetnhưng không bị chế nhạo trong thử nghiệm tích hợp. Có vẻ như vậy. Mùa xuân 3.1.2; Mockito 1.9.5)
Arjan

Mặc dù tôi chưa thử, tôi tin rằng nó sẽ hoạt động. @Autowiredđược xử lý bởi Spring JUnit Runner, được xử lý trước đó setup(). @InjectMocksđược xử lý bởi MockitoAnnotaitons.initMocks(this)trong setup(). Tôi không thấy có lý do gì để ngăn nó hoạt động. Tôi có thể thử nó để xác nhận xem nó có hoạt động không. :)
Adrian Shum

7
Tôi đã bao gồm MockitoAnnotations.initMocks(this)trong @Before. Ngoài ra, việc loại bỏ @Autowired(do đó chỉ để lại @InjectMockstại chỗ) không mang lại cho tôi mô hình TestTarget. (Nhưng việc loại bỏ @Autowiredcũng làm cho tất cả các hạt đậu khác không được khởi tạo bởi Spring.) Tuy nhiên, một số điều tra thêm cho thấy rằng tôi cần một TestTarget#setFoo(Foo f)phương pháp. Không có nó, @InjectMockshoạt động tốt, trừ khi kết hợp với @Autowired. Vì vậy: khi sử dụng cả hai @Autowired@InjectMocks, thì @Autowired private Foo mockFoo;không đủ. Có thể cần một báo cáo lỗi; sẽ điều tra.
Arjan

1
Thật không may, nó trọng nếu một người sử dụng @InjectMocks TestTarget suthoặc @Autowired @InjectMocks TestTarget sut. Đối với trước đây, Spring không tiêm bất cứ thứ gì vào TestTarget(như mong đợi) và nó đủ nếu TestTargetxác định các thuộc tính riêng tư để Mockito tiêm hạt đậu của nó. Đối với cái thứ hai, TestTargetcần một bộ thiết lập công khai cho mỗi Mockito đậu được cho là sẽ tiêm. Sử dụng sau này cho một mục tiêu mà không định nghĩa một setter nào cho Foovừa được tôi mùa xuân tiêm đậu ở TestTarget, nơi mùa xuân-tiêm Foođược không thay thế bằng mô hình Mockito.
Arjan

6

Câu hỏi của bạn dường như hỏi về cách tiếp cận ưa thích trong ba ví dụ mà bạn đã đưa ra.

Ví dụ 1 sử dụng Reflection TestUtils không phải là một cách tiếp cận tốt cho Unit testing . Bạn thực sự không muốn tải bối cảnh mùa xuân cho một bài kiểm tra đơn vị. Chỉ cần mô phỏng và tiêm những gì được yêu cầu như trong các ví dụ khác của bạn.

Bạn muốn tải ngữ cảnh mùa xuân nếu bạn muốn thực hiện một số thử nghiệm Tích hợp , tuy nhiên, tôi muốn sử dụng @RunWith(SpringJUnit4ClassRunner.class)để thực hiện tải ngữ cảnh cùng với @Autowirednếu bạn cần truy cập rõ ràng vào 'bean của nó.

Ví dụ 2 là một cách tiếp cận hợp lệ và việc sử dụng @RunWith(MockitoJUnitRunner.class)sẽ loại bỏ nhu cầu chỉ định phương thức @Before và một lệnh gọi rõ ràng tớiMockitoAnnotations.initMocks(this);

Ví dụ 3 là một cách tiếp cận hợp lệ khác không sử dụng @RunWith(...). Bạn chưa khởi tạo lớp của mình đang được kiểm tra HelloFacadeImplmột cách rõ ràng, nhưng bạn có thể làm như vậy với Ví dụ 2.

Đề xuất của tôi là sử dụng Ví dụ 2 cho thử nghiệm đơn vị của bạn vì nó làm giảm sự lộn xộn của mã. Bạn có thể quay lại cấu hình chi tiết hơn nếu và khi bạn buộc phải làm như vậy.


4

Việc giới thiệu một số phương tiện thử nghiệm mới trong Spring 4.2.RC1 cho phép người ta viết các bài kiểm tra tích hợp Spring mà không dựa vào SpringJUnit4ClassRunner. Kiểm tra cái này phần của tài liệu.

Trong trường hợp của bạn, bạn có thể viết thử nghiệm tích hợp Spring của mình và vẫn sử dụng các mô hình như sau:

@RunWith(MockitoJUnitRunner.class)
@ContextConfiguration("test-app-ctx.xml")
public class FooTest {

    @ClassRule
    public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule();

    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();

    @Autowired
    @InjectMocks
    TestTarget sut;

    @Mock
    Foo mockFoo;

    @Test
    public void someTest() {
         // ....
    }
}

1
Tại đây youtube.com/watch?v=-_aWK8T_YMI người ta có thể xem video về Spring Framework trên Java 8.
alexsmail

2

Bạn không thực sự cần MockitoAnnotations.initMocks(this);nếu bạn đang sử dụng mockito 1.9 (hoặc mới hơn) - tất cả những gì bạn cần là:

@InjectMocks
private MyTestObject testObject;

@Mock
private MyDependentObject mockedObject;

Các @InjectMockschú thích sẽ bơm tất cả mocks của mình vào MyTestObjectđối tượng.


10
Việc sử dụng @RunWith(MockitoJUnitRunner.class)nó loại bỏ nhu cầu gọi MockitoAnnotations.initMocks(this)một cách rõ ràng. Như mô tả ở đây các chú thích đã có sẵn từ 1.8.3
Brad

1
@Brad Chạy với người chạy Mockito không phải lúc nào cũng có thể thực hiện được, chỉ có vậy thôi.
Dave Newton

2

Đây là bản tóm tắt ngắn của tôi.

Nếu bạn muốn viết một bài kiểm tra đơn vị, đừng sử dụng một Spring applicationContext vì bạn không muốn bất kỳ phụ thuộc thực sự nào được đưa vào trong lớp mà bạn đang kiểm tra đơn vị. Thay vào đó, hãy sử dụng mocks, với @RunWith(MockitoJUnitRunner.class)chú thích ở đầu lớp hoặc với MockitoAnnotations.initMocks(this)trong phương thức @Before.

Nếu bạn muốn viết một bài kiểm tra tích hợp, hãy sử dụng:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("yourTestApplicationContext.xml")

Để thiết lập ngữ cảnh ứng dụng của bạn với cơ sở dữ liệu trong bộ nhớ chẳng hạn. Thông thường, bạn không sử dụng mocks trong các bài kiểm tra tích hợp, nhưng bạn có thể làm điều đó bằng cách sử dụng MockitoAnnotations.initMocks(this)phương pháp được mô tả ở trên.


0

Sự khác biệt về việc bạn có phải khởi tạo @InjectMockstrường chú thích của mình là trong phiên bản Mockito, không phải ở việc bạn sử dụng MockitoJunitRunner hay MockitoAnnotations.initMocks. Trong 1.9, nó cũng sẽ xử lý một số phương thức chèn vào @Mockcác trường của bạn , nó sẽ thực hiện việc khởi tạo cho bạn. Trong các phiên bản trước đó, bạn phải tự khởi chạy nó.

Đây là cách tôi thực hiện thử nghiệm đơn vị đối với đậu Mùa xuân của mình. Không có vấn đề gì cả. Mọi người bối rối khi họ muốn sử dụng các tệp cấu hình Spring để thực sự thực hiện việc đưa các mô phỏng, điều này đang vượt qua điểm của các bài kiểm tra đơn vị và kiểm tra tích hợp.

tất nhiên đơn vị được thử nghiệm là Impl. Bạn cần phải kiểm tra một điều cụ thể thực sự, phải không? Ngay cả khi bạn đã khai báo nó là một giao diện, bạn sẽ phải khởi tạo giao diện thực để kiểm tra nó. Bây giờ, bạn có thể tham gia vào các gián điệp, đó là các trình bao bọc sơ khai / mô phỏng xung quanh các đối tượng thực, nhưng điều đó nên dành cho các trường hợp góc.


Tôi thực sự muốn kiểm tra "hợp đồng" được xác định trong giao diện. Nếu Impl của tôi tình cờ có một số phương thức công khai không được hiển thị qua giao diện, đối với tôi nó giống như phương thức riêng tư và tôi có xu hướng bỏ qua nó.
alexsmail

Nhưng những gì hoàn thành "hợp đồng"? Impl, phải không? Vì vậy, đó là những gì bạn kiểm tra. Nó thậm chí sẽ trông như thế nào nếu chỉ kiểm tra giao diện? Bạn không thể khởi tạo một giao diện.
jhericks

Tôi đã mở câu hỏi mới về vấn đề này stackoverflow.com/questions/10937763/…
alexsmail Ngày

OK, rõ ràng từ câu hỏi khác của bạn rằng chúng ta chỉ đang nhầm lẫn với nhau về từ vựng hoặc điều gì đó. Tất nhiên trường hợp bạn đang thử nghiệm là Impl, nhưng bạn khai báo nó là giao diện hay Impl? Tôi nghĩ rằng đó là ngữ cảnh cụ thể, nhưng nó chắc chắn ít vô nghĩa hơn những gì tôi nghĩ bạn đang hỏi ban đầu.
jhericks

0

Nếu bạn muốn chuyển dự án của mình sang Spring Boot 1.4, bạn có thể sử dụng chú thích mới @MockBeanđể làm giả MyDependentObject. Với tính năng đó, bạn có thể xóa Mockito @Mock@InjectMockschú thích khỏi bài kiểm tra của mình.

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.