Mockito matchers hoạt động như thế nào?


122

Quẹt luận Mockito (ví dụ như any, argThat, eq, same, và ArgumentCaptor.capture()) cư xử rất khác so với quẹt hamcrest.

  • Trình so khớp Mockito thường gây ra lỗi không hợp lệ, ngay cả trong mã thực thi lâu sau khi bất kỳ trình kết hợp nào được sử dụng.

  • Các trình so khớp Mockito phải tuân theo các quy tắc kỳ lạ, chẳng hạn như chỉ yêu cầu sử dụng trình so khớp Mockito cho tất cả các đối số nếu một đối số trong một phương thức nhất định sử dụng trình so khớp.

  • Trình so khớp Mockito có thể gây ra NullPointerException khi ghi đè Answercác s hoặc khi sử dụng, (Integer) any()v.v.

  • Cấu trúc lại mã bằng trình so khớp Mockito theo những cách nhất định có thể tạo ra các ngoại lệ và hành vi không mong muốn và có thể thất bại hoàn toàn.

Tại sao các trình so khớp Mockito lại được thiết kế như vậy và chúng được triển khai như thế nào?

Câu trả lời:


236

Mockito so khớp là các phương thức tĩnh và các cuộc gọi đến các phương thức đó, các phương thức này thay thế cho các đối số trong khi gọi đến whenverify.

Trình so khớp Hamcrest (phiên bản lưu trữ) (hoặc Trình so khớp kiểu Hamcrest) là các trường hợp đối tượng không trạng thái, có mục đích chung, triển khai Matcher<T>và hiển thị một phương thức matches(T)trả về true nếu đối tượng phù hợp với tiêu chí của Matcher. Chúng nhằm mục đích không có tác dụng phụ và thường được sử dụng trong các khẳng định như dưới đây.

/* Mockito */  verify(foo).setPowerLevel(gt(9000));
/* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000)));

Các trình so khớp Mockito tồn tại, tách biệt với các trình so khớp kiểu Hamcrest, để các mô tả của các biểu thức so khớp phù hợp trực tiếp với các lệnh gọi phương thức : Mockito so khớp trả về Tnơi các phương thức Hamcrest matcher trả về các đối tượng Matcher (thuộc loại Matcher<T>).

Quẹt Mockito được gọi thông qua phương pháp tĩnh như eq, any, gt, và startsWithtrên org.mockito.Matchersorg.mockito.AdditionalMatchers. Ngoài ra còn có các bộ điều hợp, đã thay đổi trên các phiên bản Mockito:

  • Đối với Mockito 1.x, Matchersmột số lệnh gọi (chẳng hạn như intThathoặc argThat) là các trình so khớp Mockito trực tiếp chấp nhận các trình so khớp Hamcrest làm tham số. ArgumentMatcher<T>mở rộng org.hamcrest.Matcher<T>, được sử dụng trong biểu diễn Hamcrest nội bộ và là lớp cơ sở của Hamcrest matcher thay vì bất kỳ loại nào của Mockito matcher.
  • Đối với Mockito 2.0+, Mockito không còn phụ thuộc trực tiếp vào Hamcrest. Matchersgọi phrased as intThathoặc argThatwrap ArgumentMatcher<T>đối tượng không còn triển khai org.hamcrest.Matcher<T>nhưng được sử dụng theo những cách tương tự. Bộ điều hợp Hamcrest chẳng hạn như argThatintThatvẫn có sẵn, nhưng MockitoHamcrestthay vào đó đã chuyển sang .

Bất kể người ghép là Hamcrest hay đơn giản là kiểu Hamcrest, chúng có thể được điều chỉnh như vậy:

/* Mockito matcher intThat adapting Hamcrest-style matcher is(greaterThan(...)) */
verify(foo).setPowerLevel(intThat(is(greaterThan(9000))));

Trong câu lệnh trên: foo.setPowerLevel là một phương thức chấp nhận một int. is(greaterThan(9000))trả về a Matcher<Integer>, sẽ không hoạt động như một setPowerLevelđối số. Mockito matcher intThatkết thúc Matcher kiểu Hamcrest đó và trả về một intđể nó có thể xuất hiện như một đối số; Những người so khớp Mockito gt(9000)muốn gói toàn bộ biểu thức đó thành một lệnh gọi, như trong dòng đầu tiên của mã ví dụ.

Những người đối sánh làm gì / quay lại

when(foo.quux(3, 5)).thenReturn(true);

Khi không sử dụng trình so khớp đối số, Mockito ghi lại các giá trị đối số của bạn và so sánh chúng với các equalsphương thức của chúng .

when(foo.quux(eq(3), eq(5))).thenReturn(true);    // same as above
when(foo.quux(anyInt(), gt(5))).thenReturn(true); // this one's different

Khi bạn gọi một đối sánh như anyhoặc gt(lớn hơn), Mockito lưu trữ một đối tượng đối sánh khiến Mockito bỏ qua kiểm tra bình đẳng đó và áp dụng đối sánh bạn chọn. Trong trường hợp của argumentCaptor.capture()nó, nó lưu trữ một trình so khớp để lưu đối số của nó để kiểm tra sau này.

Trình so khớp trả về các giá trị giả chẳng hạn như số không, bộ sưu tập trống hoặc null. Mockito cố gắng trả về một giá trị giả thích hợp, an toàn, như 0 cho anyInt()hoặc any(Integer.class)hoặc một giá trị trống List<String>cho anyListOf(String.class). Tuy nhiên, vì tính năng xóa kiểu, Mockito thiếu thông tin kiểu để trả về bất kỳ giá trị nào nhưng nullany()hoặc argThat(...), có thể gây ra NullPointerException nếu cố gắng "tự động mở hộp" một nullgiá trị nguyên thủy.

Đối sánh thích eqgtlấy các giá trị tham số; lý tưởng nhất, các giá trị này nên được tính toán trước khi bắt đầu khai báo / xác minh. Gọi một người giả đang chế giễu một cuộc gọi khác có thể ảnh hưởng đến việc khai gian.

Không thể sử dụng các phương thức đối sánh làm giá trị trả về; không có cách nào để diễn đạtthenReturn(anyInt()) hoặc thenReturn(any(Foo.class))trong Mockito, chẳng hạn. Mockito cần biết chính xác thể hiện nào để trả về trong các cuộc gọi sơ khai và sẽ không chọn giá trị trả về tùy ý cho bạn.

Chi tiết triển khai

Các trình so khớp được lưu trữ (dưới dạng trình so khớp đối tượng kiểu Hamcrest) trong một ngăn xếp chứa trong một lớp được gọi là ArgumentMatcherStorage . MockitoCore và Matchers đều sở hữu một cá thể ThreadSafeMockingProgress , trong đó tĩnh chứa các cá thể MockingProgress đang giữ ThreadLocal. Đó là MockingProgressImpl này giữ một bê tông ArgumentMatcherStorageImpl . Do đó, trạng thái mô phỏng và đối sánh là tĩnh nhưng được phân luồng nhất quán giữa các lớp Mockito và Matchers.

Hầu hết các cuộc gọi khớp chỉ làm tăng thêm chồng này, với một ngoại lệ cho quẹt như and, or, vànot . Điều này hoàn toàn tương ứng với (và dựa vào) thứ tự đánh giá của Java , thứ tự đánh giá các đối số từ trái sang phải trước khi gọi một phương thức:

when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true);
[6]      [5]  [1]       [4] [2]     [3]

Điều này sẽ:

  1. Thêm anyInt()vào ngăn xếp.
  2. Thêm gt(10)vào ngăn xếp.
  3. Thêm lt(20)vào ngăn xếp.
  4. Di chuyển gt(10)lt(20)và thêm and(gt(10), lt(20)).
  5. Cuộc gọi foo.quux(0, 0), mà (trừ khi được khai báo khác) trả về giá trị mặc định false. Mockito nội bộ đánh dấu quux(int, int)là cuộc gọi gần đây nhất.
  6. Lệnh gọi when(false), loại bỏ đối số của nó và chuẩn bị khai báo phương thức quux(int, int)được xác định trong 5. Hai trạng thái hợp lệ duy nhất là với độ dài ngăn xếp là 0 (bằng nhau) hoặc 2 (đối sánh) và có hai đối sánh trên ngăn xếp (bước 1 và 4), vì vậy Mockito khai báo phương thức với một trình any()khớp nối cho đối số đầu tiên và and(gt(10), lt(20))đối số thứ hai và xóa ngăn xếp.

Điều này thể hiện một số quy tắc:

  • Mockito không thể phân biệt được giữa quux(anyInt(), 0)quux(0, anyInt()). Cả hai đều giống như một cuộc gọi đến quux(0, 0)với một int matcher trên ngăn xếp. Do đó, nếu bạn sử dụng một trình so khớp, bạn phải khớp với tất cả các đối số.

  • Thứ tự cuộc gọi không chỉ quan trọng mà nó là thứ làm cho tất cả điều này hoạt động . Việc trích xuất các trình so khớp vào các biến thường không hoạt động, vì nó thường thay đổi thứ tự cuộc gọi. Tuy nhiên, việc trích xuất các đối sánh thành các phương thức hoạt động rất tốt.

    int between10And20 = and(gt(10), lt(20));
    /* BAD */ when(foo.quux(anyInt(), between10And20)).thenReturn(true);
    // Mockito sees the stack as the opposite: and(gt(10), lt(20)), anyInt().
    
    public static int anyIntBetween10And20() { return and(gt(10), lt(20)); }
    /* OK */  when(foo.quux(anyInt(), anyIntBetween10And20())).thenReturn(true);
    // The helper method calls the matcher methods in the right order.
  • Ngăn xếp thay đổi thường xuyên đến mức Mockito không thể cảnh sát nó cẩn thận. Nó chỉ có thể kiểm tra ngăn xếp khi bạn tương tác với Mockito hoặc một mô hình và phải chấp nhận các đối tác mà không cần biết liệu chúng được sử dụng ngay lập tức hay bị bỏ rơi vô tình. Về lý thuyết, ngăn xếp phải luôn trống bên ngoài lệnh gọi đến whenhoặc verify, nhưng Mockito không thể tự động kiểm tra điều đó. Bạn có thể kiểm tra thủ công với Mockito.validateMockitoUsage().

  • Trong một lệnh gọi tới when, Mockito thực sự gọi phương thức được đề cập, phương thức này sẽ ném ra một ngoại lệ nếu bạn đã khai báo phương thức để ném một ngoại lệ (hoặc yêu cầu các giá trị khác 0 hoặc không rỗng). doReturndoAnswer(v.v.) làm không gọi phương thức thực tế và thường là một phương pháp thay thế hữu ích.

  • Nếu bạn đã gọi một phương pháp giả ở giữa stubbing (ví dụ như để tính toán một câu trả lời cho một eqkhớp), Mockito sẽ kiểm tra độ dài đống chống lại rằng cuộc gọi thay vào đó, và có khả năng thất bại.

  • Nếu bạn cố gắng làm điều gì đó xấu, chẳng hạn như khai báo / xác minh một phương thức cuối cùng , Mockito sẽ gọi phương thức thực và cũng để lại các đối sánh bổ sung trên ngăn xếp . Lệnh finalgọi phương thức có thể không đưa ra một ngoại lệ, nhưng bạn có thể nhận được một lỗi không hợp lệ từ các đối tượng trùng khớp khi tiếp theo bạn tương tác với một mô hình.

Vấn đề chung

  • Không hợp lệUseOfMatchersException :

    • Kiểm tra để đảm bảo rằng mọi đối số đều có chính xác một lệnh gọi đối sánh, nếu bạn hoàn toàn sử dụng đối sánh và bạn chưa sử dụng một đối sánh bên ngoài lệnh gọi whenhoặc verify. Các đối sánh không bao giờ được sử dụng làm giá trị trả về gốc hoặc trường / biến.

    • Kiểm tra để đảm bảo rằng bạn không gọi một mô hình như một phần của việc cung cấp đối số so khớp.

    • Kiểm tra để đảm bảo rằng bạn không cố gắng khai báo / xác minh một phương pháp cuối cùng bằng trình so khớp. Đó là một cách tuyệt vời để để một trình so khớp trên ngăn xếp và trừ khi phương pháp cuối cùng của bạn đưa ra một ngoại lệ, đây có thể là lần duy nhất bạn nhận ra phương pháp bạn đang chế nhạo là cuối cùng.

  • NullPointerException với các đối số nguyên thủy: (Integer) any() trả về null trong khi any(Integer.class)trả về 0; điều này có thể gây ra NullPointerExceptionnếu bạn đang mong đợi một intthay vì một Số nguyên. Trong mọi trường hợp, hãy ưu tiên anyInt(), nó sẽ trả về 0 và cũng bỏ qua bước tự động đấm bốc.

  • NullPointerException hoặc các ngoại lệ khác: Các lệnh gọi đến when(foo.bar(any())).thenReturn(baz)sẽ thực sự gọi foo.bar(null) , mà bạn có thể đã cố gắng ném một ngoại lệ khi nhận đối số null. Chuyển để doReturn(baz).when(foo).bar(any()) bỏ qua hành vi bị khai thác .

Xử lý sự cố chung

  • Sử dụng MockitoJUnitRunner , hoặc gọi một cách rõ ràng validateMockitoUsagetrong phương thức tearDownhoặc của bạn @After(mà trình chạy sẽ tự động làm cho bạn). Điều này sẽ giúp xác định xem bạn có sử dụng sai các đối sánh hay không.

  • Đối với mục đích gỡ lỗi, hãy thêm lệnh gọi validateMockitoUsagetrực tiếp vào mã của bạn. Điều này sẽ ném ra nếu bạn có bất kỳ thứ gì trên ngăn xếp, đó là một cảnh báo tốt về một triệu chứng xấu.


2
Cảm ơn cho bài viết này. Một NullPointerException với định dạng when / thenReturn đã gây ra sự cố cho tôi, cho đến khi tôi thay đổi nó thành doReturn / when.
yngwietiger

11

Chỉ là một bổ sung nhỏ cho câu trả lời xuất sắc của Jeff Bowman, khi tôi tìm thấy câu hỏi này khi tìm kiếm giải pháp cho một trong những vấn đề của riêng tôi:

Nếu một lệnh gọi đến một phương thức khớp với nhiều hơn một whenlệnh gọi được huấn luyện của mô hình , thứ tự của các whenlệnh gọi là quan trọng và phải từ rộng nhất đến cụ thể nhất. Bắt đầu từ một trong những ví dụ của Jeff:

when(foo.quux(anyInt(), anyInt())).thenReturn(true);
when(foo.quux(anyInt(), eq(5))).thenReturn(false);

là thứ tự đảm bảo kết quả (có thể) mong muốn:

foo.quux(3 /*any int*/, 8 /*any other int than 5*/) //returns true
foo.quux(2 /*any int*/, 5) //returns false

Nếu bạn đảo ngược các cuộc gọi when thì kết quả sẽ luôn là true.


2
Mặc dù đây là thông tin hữu ích, nhưng nó liên quan đến sơ khai, không phải đối sánh , vì vậy nó có thể không có ý nghĩa đối với câu hỏi này. Thứ tự thực sự quan trọng, nhưng chỉ ở chỗ chuỗi kết hợp được xác định cuối cùng thắng : Điều này có nghĩa là các sơ khai cùng tồn tại thường được khai báo là cụ thể nhất, nhưng trong một số trường hợp, bạn có thể muốn ghi đè rất rộng hành vi được chế nhạo cụ thể trong một trường hợp thử nghiệm. , tại thời điểm đó, một định nghĩa rộng có thể cần đến cuối cùng.
Jeff Bowman

1
@JeffBowman Tôi nghĩ câu hỏi này có ý nghĩa vì câu hỏi là về các trình so khớp mockito và các trình kết hợp có thể được sử dụng khi khai thác (như trong hầu hết các ví dụ của bạn). Kể từ khi tìm kiếm trên google để tìm lời giải thích, tôi đã đưa ra câu hỏi này, tôi nghĩ rằng sẽ hữu ích khi có thông tin này ở đây.
tibtof
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.