برنامه نویسی

Argument Matcherهای Mockito: any()، eq() و موارد دیگر

Argument Matcherهای Mockito به شما اجازه می‌دهند فراخوانی‌های متد را Stub و Verify کنید—بدون Hard-code کردن هر مقدارِ آرگومانِ Mockito. وقتی Matcherها را با when() و verify() جفت می‌کنید، می‌توانید قوانینی مانند «هر رشته‌ای» یا «این ID دقیق» را بیان کنید؛ در حالی که تست‌ها خوانا می‌مانند. این راهنما، Matcherهای داخلی، قاعده سازگاری که از InvalidUseOfMatchersException جلوگیری می‌کند، ArgumentCaptor و پیاده‌سازی‌های ArgumentMatcher سفارشی را—با استفاده از APIهای فعلی Mockito 5.x با JUnit 5—مرور می‌کند.

نکات کلیدی

  • Argument Matcherهای Mockito در org.mockito.ArgumentMatchers زندگی می‌کنند و فقط درون when()، verify() و helperهای Stubbing مرتبط مانند doNothing().when() کار می‌کنند.
  • اگر آرگومانی از Matcher استفاده کند، هر آرگومانی در آن فراخوانی باید از Matcher استفاده کند. Literalها را با eq() بپیچید—به‌جای پاس دادن مقادیر خام.
  • Matcherهای تایپ‌شده (anyString()، anyInt()، anyList()) را به any() خام برای Primitiveها و Referenceها ترجیح دهید تا از غافلگیری‌های null/Unboxing اجتناب کنید.
  • از ArgumentCaptor وقتی استفاده کنید که لازم است بعد از فراخوانی ببینید چه چیزی پاس شده. از argThat() با ArgumentMatcher سفارشی وقتی استفاده کنید که منطق Matching قابل استفاده مجددِ Inline لازم دارید.
  • Matcherها انتظارات را روی Stack داخلی ثبت و مقادیر ساختگی (Dummy) برمی‌گردانند؛ پس هرگز بیرون از عبارت Stubbing یا Verification صدا‌شان نزنید.

پیش‌نیازها

  • Java 11 یا جدیدتر. اگر Runtime محلی لازم دارید، راهنمای «نحوه نصب جاوا با Apt روی اوبونتو» در پارمین کلود را دنبال کنید.
  • پروژه Maven یا Gradle با JUnit 5 و Mockito 5.x. مثال‌های زیر از Mockito 5.14.2 و JUnit Jupiter 5.10.2 استفاده می‌کنند.
  • آشنایی با Mockهای پایه از راهنمای «مثال‌های Mock در Mockito» و Verification از راهنمای «Verify در Mockito» در پارمین کلود.
  • پس‌زمینه اختیاری: راهنمای «آموزش JUnit 5» و راهنمای گسترده‌تر «آموزش Mockito» در پارمین کلود.

وابستگی‌های تست Maven:

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>5.10.2</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-junit-jupiter</artifactId>
  <version>5.14.2</version>
  <scope>test</scope>
</dependency>

Import کردن Matcherها به‌صورت Static در کلاس‌های تست:

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

Argument Matcherهای Mockito چیستند؟

Argument Matcherهای Mockito، متدهای Helperای هستند که شرایط انعطاف‌پذیری را برای پارامترهای متد حین Stubbing یا Verification توصیف می‌کنند. به‌جای Matching با equals() روی مقادیر دقیق، می‌توانید هر String، هر int مثبت یا قانون دامنه‌ای سفارشی را بپذیرید.

Argument Matcherها چگونه در فریم‌ورک Mockito جای می‌گیرند

Mockito، Matcherها را روی Stack داخلی‌ای ثبت می‌کند وقتی Stub یا Verificationای می‌سازید. متدهایی مانند anyInt() مقادیر Dummy امنی برمی‌گردانند (مثلاً 0 برای anyInt()) تا کامپایلر جاوا عبارتِ درون when(mock.get(anyInt())) را بپذیرد. Matching واقعی وقتی رخ می‌دهد که Mockito Matcherهای ثبت‌شده را در برابر آرگومان‌های فراخوانی واقعی بازپخش کند. مرجع رسمی: Javadoc مربوط به ArgumentMatchers.

چه زمانی از Matcher در مقابل مقادیر دقیق استفاده کنیم

از مقادیر دقیق (یا eq()) وقتی استفاده کنید که تست به یک ورودی خاص اهمیت می‌دهد. از Matcherها وقتی استفاده کنید که مقدار بی‌اهمیت است، بین موارد متغیر است یا از الگویی پیروی می‌کند (نوع، بازه، زیررشته). استفاده بیش‌ازحد از any() می‌تواند باگ‌ها را پنهان کند؛ پس پیش‌فرض روی مقادیر دقیق باشید و وقتی Matcherها تکرار را کم می‌کنند سراغ‌شان بروید.

کلاس نمونه استفاده‌شده در سراسر این آموزش:

public class Foo {
    public boolean bool(String str, int i, Object obj) {
        return false;
    }

    public int in(boolean b, java.util.List<String> strs) {
        return 0;
    }

    public int bar(byte[] bytes, String[] s, int i) {
        return 0;
    }

    public void log(String message) {
        // no-op
    }
}

قاعده سازگاری Argument Matcher

Mockito الزامی می‌کند که یا همه آرگومان‌ها در Stubbing یا Verification از Matcher استفاده کنند، یا هیچ‌کدام. ترکیب Matcher با Literal خام، InvalidUseOfMatchersException را Trigger می‌کند.

چرا نمی‌توان Matcherها و مقادیر دقیق را ترکیب کرد

Matcherها و Literalها مسیرهای کدی متفاوتی درون Mockito دنبال می‌کنند. فراخوانی‌ای مثل when(mock.bool(anyString(), 1, any())) نامعتبر است؛ چون 1 مقدار خام است در حالی که بقیه پارامترها از Matcher استفاده می‌کنند.

نادرست:

when(mockFoo.bool(anyString(), 1, any(Object.class))).thenReturn(true); // پرتاب می‌کند

درست:

when(mockFoo.bool(anyString(), eq(1), any(Object.class))).thenReturn(true);

همین قاعده روی verify() هم اعمال می‌شود:

verify(mockFoo).bool(eq("hello"), anyInt(), any(Object.class));

نحوه رفع InvalidUseOfMatchersException

  • هر Literal را با eq() بپیچید: eq(1) یا eq("hello").
  • یا همه Matcherها را حذف و فقط مقادیر مشخص پاس بدهید.
  • هرگز نتایج Matcher را در متغیرها ذخیره و بین Stubbingها استفاده مجدد نکنید.

Argument Matcherهای داخلی در Mockito

جدول زیر Matcherهای پرکاربرد در Mockito 5.x را خلاصه می‌کند. برای Referenceهای null، Matcherهای تایپ‌شده‌ای مانند anyString() با null تطبیق نمی‌یابند؛ به‌جایش از isNull() یا isNotNull() استفاده کنید:

Matcherانواع پذیرفته‌شدهرفتار nullمورد استفاده معمول
any()هر Reference، Varargsnull را مجاز می‌داندMatching loosen Reference
any(Class)نوع Tnull را مستثنی می‌کندMatching آبجکتِ Type-Safe
eq(value)همان نوعِ valueاز equals() پیروی می‌کندمقدار دقیق درون زنجیره‌های Matcher
anyString()Stringnull را مستثنی می‌کندهر رشته غیر-null
anyInt()int یا IntegerWrapperِ null را مستثنی می‌کندint Primitive یا Boxed
anyList()Listnull را مستثنی می‌کندهر List غیر-null
isNull() / isNotNull()انواع Referenceچک‌های null صریحپارامترهای Nullable
contains()، startsWith()، endsWith()Stringnull را مستثنی می‌کندMatching جزئی رشته

استفاده از any() و any(Class) برای Matching مبتنی بر تایپ

any() با هر Referenceای—including null—مطابقت می‌یابد (و Varargs را پشتیبانی می‌کند). any(Foo.class) بررسی نوعی انجام و null را رد می‌کند. وقتی شفافیت مهم است any(Class) را ترجیح دهید:

Foo mockFoo = mock(Foo.class);
when(mockFoo.bool(anyString(), anyInt(), any(Object.class))).thenReturn(true);

assertTrue(mockFoo.bool("A", 1, "A"));
assertTrue(mockFoo.bool("B", 10, new Object()));

برای آرایه‌ها، کلاسِ آرایه را پاس بدهید:

when(mockFoo.bar(any(byte[].class), any(String[].class), anyInt())).thenReturn(1);

استفاده از eq() برای تطبیق مقادیر دقیق درون زنجیره‌های Matcher

وقتی هر پارامتری از Matcher استفاده می‌کند، مقادیر خاص را با eq() بپیچید:

when(mockFoo.bool(eq("false"), anyInt(), any(Object.class))).thenReturn(false);
assertFalse(mockFoo.bool("false", 10, new Object()));

بدون Matcherهای دیگر، می‌توانید Literalها را مستقیم پاس بدهید: when(mockFoo.bool("false", 10, obj)).

استفاده از anyString()، anyInt()، anyList() و سایر Matcherهای تایپ‌شده

Matcherهای تایپ‌شده خوانایی را بهبود و از دام‌های Autoboxing روی Primitiveها اجتناب می‌کنند:

when(mockFoo.in(anyBoolean(), anyList())).thenReturn(10);

برای Collectionها و Mapها هم anySet()، anyMap() و anyCollection() وجود دارند.

استفاده از isNull() و isNotNull()

when(mockFoo.bool(isNull(), anyInt(), isNotNull())).thenReturn(true);
assertTrue(mockFoo.bool(null, 1, "payload"));

استفاده از contains()، startsWith() و endsWith() برای Matching رشته

when(mockFoo.bool(startsWith("ERR"), anyInt(), any())).thenReturn(false);
mockFoo.bool("ERR-404", 0, null);
verify(mockFoo).bool(contains("ERR"), anyInt(), any());

استفاده از Argument Matcherها با when() برای Stubbing

Stubbing با any() و eq() با هم

Foo mockFoo = mock(Foo.class);
when(mockFoo.bool(anyString(), anyInt(), any(Object.class))).thenReturn(true);
when(mockFoo.bool(eq("false"), anyInt(), any(Object.class))).thenReturn(false);

Stubbing به ترتیب اعلان ارزیابی می‌شود؛ اگر الگوها هم‌پوشانی دارند، خاص‌ترین Stub باید آخر ظاهر شود.

Stubbing متدهای چندپارامتری (متدهای void)

برای متدهای void، از doNothing() استفاده کنید تا Matcherها به همان شکل کار کنند:

doNothing().when(mockFoo).log(anyString());
mockFoo.log("ready");

استفاده از Argument Matcherها با verify() برای Verification رفتار

Argument Matcherها فقط با verify() (و APIهای Stubbing) کار می‌کنند—نه به‌عنوان چک‌های بولی عمومی.

Verify کردن فراخوانی متدی با انواع آرگومان خاص (انواع پارامتر)

verify(mockFoo, atLeastOnce()).bool(anyString(), anyInt(), any(Object.class));
verify(mockFoo).bool(eq("false"), anyInt(), any(Object.class));

ترکیب verify() با times()، never() و atLeast()

verify(mockFoo, times(1)).log(anyString());
verify(mockFoo, never()).bool(eq("skip"), anyInt(), any());
verify(mockFoo, atLeast(0)).in(anyBoolean(), anyList());

برای Assert کردن ترتیب فراخوانی، از InOrder استفاده کنید:

InOrder inOrder = inOrder(mockFoo);
inOrder.verify(mockFoo).log(anyString());
inOrder.verify(mockFoo).bool(anyString(), anyInt(), any());

ثبت آرگومان‌ها با ArgumentCaptor

ArgumentCaptor آرگومان‌های پاس‌شده به Mock را ثبت می‌کند تا بعد از واقع شدن روی آن‌ها Assert بگیرید. مکمل Matcherهاست: Matcherها قوانین پذیرش را بیان می‌کنند؛ Captorها مقادیر واقعی را بازرسی می‌کنند.

چه زمانی از ArgumentCaptor به‌جای Matcher استفاده کنیم

از Captor وقتی استفاده کنید که به چند Assertion روی همان آبجکت بعد از فراخوانی نیاز دارید (فیلدها، حجم، مقادیر مشتق‌شده). از Matcherها وقتی استفاده کنید که Predicateای حین Stubbing یا Verification کافی است.

ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
mockFoo.log("shipped");
verify(mockFoo).log(messageCaptor.capture());
assertEquals("shipped", messageCaptor.getValue());

ArgumentCaptor با فراخوانی‌های منفرد و متعدد

mockFoo.log("first");
mockFoo.log("second");
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
verify(mockFoo, times(2)).log(captor.capture());
assertEquals(List.of("first", "second"), captor.getAllValues());

ArgumentCaptor در مقابل ArgumentMatcher سفارشی

رویکردمناسب برایبده‌بستان
ArgumentCaptorبازرسی Instanceهای واقعی آرگومان بعد از فراخوانیقدم verify اضافی، برای Stubbing مقادیر بازگشتی نیست
argThat(ArgumentMatcher)قوانین Inline قابل استفاده مجدد حین Stub/Verifyدید کمتر به مقادیر واقعی مگر Verification شکست بخورد
eq() / any*() تایپ‌شدهتست‌های ساده و سریعبیان کمتر برای قوانین دامنه‌ای پیچیده

نوشتن ArgumentMatcher سفارشی

اینترفیس org.mockito.ArgumentMatcher<T> را وقتی پیاده کنید که Matcherهای داخلی کافی نباشند. منطق را با argThat() ثبت کنید.

پیاده‌سازی اینترفیس ArgumentMatcher

import org.mockito.ArgumentMatcher;

public class PremiumOrderMatcher implements ArgumentMatcher<Order> {
    @Override
    public boolean matches(Order order) {
        return order != null && order.getTotalCents() >= 10_000;
    }
}

مثال Matcher سفارشی با آبجکت دامنه‌ای

public class Order {
    private final int totalCents;
    public Order(int totalCents) { this.totalCents = totalCents; }
    public int getTotalCents() { return totalCents; }
}

public class OrderService {
    public boolean isPremium(Order order) {
        return order != null && order.getTotalCents() >= 10_000;
    }
}

ثبت و استفاده از Matcher سفارشی در تست‌ها

OrderService service = mock(OrderService.class);
when(service.isPremium(argThat(new PremiumOrderMatcher()))).thenReturn(true);
assertTrue(service.isPremium(new Order(15_000)));

فرم Lambda (Java 8+):

when(service.isPremium(argThat(o -> o.getTotalCents() > 5_000))).thenReturn(true);

برای نکات طراحی از تیم Mockito، Javadoc مربوط به ArgumentMatcher را ببینید.

AdditionalMatchers مربوط به Mockito

org.mockito.AdditionalMatchers مقایسه‌های عددی و Helperهای برابری آرایه فراهم می‌کند:

import static org.mockito.AdditionalMatchers.*;

when(mockFoo.bar(any(byte[].class), aryEq(new String[] { "A", "B" }), gt(10))).thenReturn(11);

assertEquals(11, mockFoo.bar("abc".getBytes(), new String[] { "A", "B" }, 20));

سایر Helperها شامل lt()، or()، and() و not() برای شرایط ترکیبی‌اند.

اشتباهات رایج و نحوه اجتناب از آن‌ها

ترکیب Matcherها و مقادیر خام

وقتی هر پارامتر خواهری از Matcher استفاده می‌کند، همیشه Literalها را با eq() تبدیل کنید.

استفاده از Argument Matcherها بیرون از when() یا verify()

String value = anyString(); // نادرست: matcher بیرون از stub/verify استفاده شده

Matcherها باید مستقیماً درون فراخوانی متدِ Mock-شده‌ای که به when() یا verify() پاس می‌شود ظاهر شوند.

استفاده بیش‌ازحد از any() و پنهان کردن باگ‌های واقعی

برای پارامترهای حیاتی-برای-کسب‌وکار (IDها، Currencyها) مقادیر دقیق را ترجیح دهید. از Matcherها برای حذف نویز استفاده کنید—نه برای رد کردن Assertionهای مهم.

Argument Matcherهای Mockito در مقابل Matcherهای Hamcrest

Mockito از Hamcrest در نسخه 2.1 جدا شد. برای استفاده از Hamcrest، artifact مربوط به mockito-hamcrest را اضافه و به‌جای APIهای منسوخِ org.mockito.Matchers از MockitoHamcrest.argThat(org.hamcrest.Matcher) صدا بزنید. برای بیشتر پروژه‌ها، داخلی‌های Mockito به‌همراه argThat(ArgumentMatcher) ساده‌تر می‌مانند و از وابستگی اضافی اجتناب می‌کنند.

ابزارنقطه ورودچه زمانی انتخاب کنیم
ArgumentMatchers مربوط به Mockitoany()، eq()، contains()انتخاب پیش‌فرض برای تست‌های Mockito
ArgumentMatcher سفارشی + argThat()argThat(predicate)قوانین خاص-دامنه‌ای مشترک بین تست‌ها
Hamcrest از طریق MockitoHamcrestMockitoHamcrest.argThat(hasItem(…))Assertionهای Hamcrest موجودی که از قبل نگه می‌دارید

انتخاب ابزار درست برای کار

نیازاستفاده
نادیده گرفتن مقدار پارامترanyString()، anyInt()، any(MyDto.class)
یک مقدار دقیق بین Matcherهاeq(“literal”)
بازرسیِ آنچه پاس شدهArgumentCaptor
قانون پیچیده قابل استفاده مجددArgumentMatcher سفارشی از طریق argThat()
Stub متد voiddoNothing().when(mock).method(any())

سوالات متداول

۱. Argument Matcherها در Mockito چیستند؟

Argument Matcherها متدهای Staticای مانند anyString()، eq() و argThat() هستند که به Mockito می‌گویند حین Stubbing یا Verification چگونه پارامترها را مقایسه کند. آن‌ها Matching انعطاف‌پذیری را وقتی مقادیر دقیق ناشناخته یا بی‌اهمیت‌اند ممکن می‌سازند. اگر برای پارامتری از Matcher استفاده کنید، هر پارامتری در آن فراخوانی باید از Matcher استفاده کند.

۲. تفاوت eq() و any() چیست؟

eq(value) آرگومانی را لازم دارد که طبق equals() (یا == برای Primitiveها) با value برابر باشد. any() و variantهای تایپ‌شده‌اش مانند anyString() طیف گسترده‌ای از ورودی‌ها را می‌پذیرند که قوانین نوع یا زیررشته را ارضا کنند. از eq() وقتی استفاده کنید که فقط یک ورودی خاص باید مطابقت یابد؛ از any*() وقتی که مقدار دقیق مهم نیست.

۳. هنگام استفاده از Matcherها، آیا همه آرگومان‌ها باید از Matcher استفاده کنند؟

بله. Mockito سازگاری Matcher را روی هر Stubbing یا فراخوانی verify() الزامی می‌کند. ترکیب anyString() با Literal خام باعث InvalidUseOfMatchersException می‌شود. Literalها را با eq() بپیچید یا برای آن فراخوانی، Matcherها را کاملاً حذف کنید.

۴. چه زمانی باید از eq() در Mockito استفاده کنم؟

از eq() وقتی استفاده کنید که حداقل یک پارامتر دیگر در همان فراخوانی از قبل از Matcher استفاده می‌کند و برای این پارامتر به مقدار دقیقی نیاز دارید. وقتی هر آرگومان Literal ساده‌ای است و Matcherای وجود ندارد، می‌توانید eq() را حذف کنید.

۵. any() در Mockito چه می‌کند؟

any() با هر Reference آبجکتی (شامل null) برای پارامترهای Reference مطابقت می‌یابد. جایگزین‌های تایپ‌شده مانند anyInt() یا any(Order.class) تطبیق‌ها را به Primitiveها یا Instanceهای کلاسی محدود می‌کنند و—از Mockito 2.1.0—null را برای آن Matcherهای تایپ‌شده مستثنی می‌کنند. برای شفافیت و تست‌های امن‌تر، Matcherهای تایپ‌شده را ترجیح دهید.

نتیجه‌گیری

Argument Matcherهای Mockito به شما کمک می‌کنند تست‌های واحد متمرکزی بنویسید—با Stub و Verify کردن قوانین پارامتری انعطاف‌پذیر با any()، eq()، Matcherهای تایپ‌شده، ArgumentCaptor و پیاده‌سازی‌های ArgumentMatcher سفارشی. قاعده همه-یا-هیچِ Matcher را به یاد داشته باشید؛ برای Primitiveها و Collectionها Matcherهای تایپ‌شده را ترجیح بدهید؛ و وقتی لازم است وضعیت واقعی آرگومان را بعد از فراخوانی بازرسی کنید، سراغ Captorها بروید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا