Mock — یک شیء جایگزین است که رفتار کامپوننت واقعی را شبیهسازی میکند و امکان بررسی تعامل با آن را فراهم میکند. برخلاف Stub که صرفاً یک مقدار مشخص را برمیگرداند، Mock واقعیت فراخوانی متد، آرگومانهای ارسالشده و تعداد فراخوانیها را ثبت میکند. طبق دادههای Mockito (2024)، Mock محبوبترین نوع Test Double در پروژههای Java و Kotlin است که در بیش از 70% تستهای واحد برنامههای موبایل استفاده میشود.
نکات اصلی
Mock — شیءای است که توسط فریمورک mocking (Mockito, MockK, EasyMock) ایجاد میشود، از یک واسط یا کلاس تقلید میکند و تمام فراخوانیهای متدهای خود را ثبت میکند. برنامهنویس انتظارات را تعیین میکند: متد X با آرگومانهای Y فراخوانی میشود و Z را برمیگرداند. پس از اجرای تست، Mock بررسی میکند که آیا انتظارات با فراخوانیهای واقعی مطابقت دارند یا خیر.
این اصطلاح از استعاره تئاتری Test Doubles گرفته شده است: Mock یک «تقلیدکننده» است که نه تنها روی صحنه میایستد (مانند Dummy)، بلکه نقش بازی میکند و بررسی میکند که تعامل با آن به درستی انجام شده است. اگر کد تستشده متدی را که Mock انتظار داشت فراخوانی نکرد یا با آرگومانهای نادرست فراخوانی کرد — تست با پیام نقض انتظار شکست میخورد.
Mock از طریق کارخانه فریمورک ایجاد میشود: mockk<MyInterface>() یا Mockito.mock(MyClass.java). فریمورک یک شیء پروکسی تولید میکند که تمام فراخوانیهای متد را رهگیری میکند. هر فراخوانی با انتظارات (expectations) از پیش تعیینشده مقایسه میشود. اگر فراخوانی با انتظار مطابقت داشت — مقدار مشخصشده برگردانده میشود. اگر نه — Mock بسته به پیکربندی، مقدار پیشفرض برمیگرداند یا استثنا پرتاب میکند.
Mock زمانی اجباری است که کد تستشده با کامپوننتهای دارای عوارض جانبی تعامل دارد: ارسال داده به سرور، نوشتن در پایگاه داده، لاگنویسی، آنالیتیکس، ناوبری، نمایش دیالوگهای سیستمی. بدون Mock این تعاملات بدون راهاندازی زیرساخت واقعی قابل بررسی نیستند. به گفته Google Testing Blog، Mock تنها راه بررسی اینکه برنامه واقعاً رویداد آنالیتیکس را ارسال کرده است بدون بالا آوردن سرور تست است.
تفاوت بین Mock و Stub یکی از بحثبرانگیزترین موضوعات در تستنویسی است. هر دو نوع وابستگی واقعی را جایگزین میکنند، اما با روشهای اساساً متفاوت.
| معیار | Mock | Stub |
|---|---|---|
| سوال اصلی | آیا متد فراخوانی شد؟ | چه نتیجهای برگردانده شد؟ |
| بررسی | رفتار (verify) | وضعیت (assert) |
| بازگشت داده | اختیاری | اجباری |
| مثال | verify(analytics).logEvent("click") | assertEquals(5, repository.getCount()) |
| زمان استفاده | عوارض جانبی | بازگشت داده |
تست ساده برای انتخاب: از خود بپرسید — «اگر این خط کد را حذف کنم، تست شکست میخورد؟». اگر تست مقدار برگشتی را بررسی میکند — Stub نیاز است (بررسی با assert). اگر تست بررسی میکند که کد متد را با آرگومانهای صحیح فراخوانی کرده است — Mock نیاز است (بررسی با verify). این دوگانگی از الگوی Command-Query Separation ناشی میشود: متدهای تغییردهنده وضعیت (commands) نیاز به Mock دارند؛ متدهای برگرداننده داده (queries) نیاز به Stub دارند.
انتخاب بین Mockito و MockK یکی از اولین تصمیمات در تنظیم پشته تست پروژه Android در Kotlin است. هر دو کتابخانه یک وظیفه را انجام میدهند، اما با رویکرد متفاوت به ویژگیهای خاص Kotlin.
Mockito — استاندارد دوفاکتو برای پروژههای Java است. نسخه 5.x به لطف MockMaker داخلی از اشیاء mock برای کلاسهای نهایی، متدهای استاتیک و سازندهها پشتیبانی میکند. برای پروژههای Kotlin، Mockito نیاز به پیکربندی اضافی دارد: پسوندهای mockito-kotlin برای نحو بهبودیافته، mockito-inline برای کلاسهای نهایی. Mockito بدون آداپتورهای اضافی از کوروتینهای Kotlin و توابع suspend پشتیبانی نمیکند.
MockK به طور خاص برای Kotlin ساخته شده است. این کتابخانه به طور بومی از کوروتینها (coEvery, coVerify)، sealed class، data class، singletonهای object و توابع extension پشتیبانی میکند. نحو MockK از DSL با بلوکهای لامبدا استفاده میکند که در کد Kotlin طبیعی به نظر میرسد. MockK همچنین میتواند بدون پیکربندی اضافی ویژگیها (property mocking) را mock کند — این برای پروژههای Android که از LiveData، StateFlow و Delegates استفاده میکنند مهم است.
// Mockito + mockito-kotlin
val repository = mock<UserRepository>()
whenever(repository.getUser(1)).thenReturn(user)
// MockK
val repository = mockk<UserRepository>()
every { repository.getUser(1) } returns user
معیارها (JVM Benchmark, 2024) نشان میدهد که MockK اشیاء mock را 15–20% سریعتر از Mockito برای پروژههای Kotlin ایجاد میکند، زیرا مستقیماً با بایتکد Kotlin کار میکند نه Java Reflections. برای پروژههایی با هزاران تست واحد، تفاوت در زمان ساخت ممکن است قابل توجه باشد: MockK در اجرای کامل تستها در پروژههای بزرگ 30–60 ثانیه صرفهجویی میکند.
سه سناریو را بررسی میکنیم: تست ViewModel با وابستگیهای Mock، تست UseCase با بررسی فراخوانی API و تست کوروتینها با coVerify.
class ProfileViewModelTest {
private val analytics = mockk<AnalyticsService>()
private val repo = mockk<UserRepository>()
private val vm = ProfileViewModel(repo, analytics)
fun `profile opened logs analytics event`() {
every { analytics.logEvent("profile_opened") } returns Unit
vm.onViewCreated()
verify { analytics.logEvent("profile_opened") }
}
}
class SendMessageUseCaseTest {
private val api = mockk<MessagingApi>()
private val useCase = SendMessageUseCase(api)
fun `send message with correct payload`() = runTest {
val message = Message(text = "Hello", userId = 42)
coEvery { api.sendMessage(any()) } returns MessageResult.Sent("msg_1")
val result = useCase.execute(message)
coVerify {
api.sendMessage(match {
it.text == "Hello" && it.userId == 42
})
}
assertTrue(result is MessageResult.Sent)
}
}
class OrderUseCaseTest {
private val api = mockk<OrderApi>()
private val useCase = OrderUseCase(api)
private val slot = slot<OrderRequest>()
fun `order request contains correct items`() = runTest {
coEvery { api.placeOrder(capture(slot)) } returns OrderResult.Placed("order_1")
useCase.execute(listOf("item_a", "item_b"))
assertEquals(2, slot.captured.items.size)
assertEquals("item_a", slot.captured.items[0])
}
}
استفاده مؤثر از Mock در توسعه موبایل نیازمند رعایت انضباط است. نقض این قوانین تستها را به مانع شکنندهای تبدیل میکند که در هر بازسازی دچار شکست میشوند.
قاعده سخت: Mock فقط برای وابستگیهایی ایجاد میشود که از مرز برنامه عبور میکنند: مشتریان API، پایگاههای داده، سیستم فایل، سرویسهای سیستمی (LocationManager, BluetoothAdapter, Camera). کلاسهای داخلی برنامه — موجودیتهای دامنه، Value Object، ابزارهای ساده — نباید با Mock جایگزین شوند. رفتار آنها از طریق اشیاء واقعی تست میشود.
هر تست باید دقیقاً یک بررسی منطقی داشته باشد — یا verify (برای Mock)، یا assert (برای Stub). بررسی وضعیت و رفتار را در یک تست مخلوط نکنید. اگر هم فراخوانی API و هم نتیجه باید بررسی شوند — دو تست جداگانه با نامهای متفاوت ایجاد کنید. این قاعده که به «یک assert در هر تست» معروف است، به توصیههای Kent Beck (2002) برمیگردد.
علاوه بر mocking پایه، تکنیکهای پیشرفتهای وجود دارند که مسائل خاص در توسعه موبایل را حل میکنند: تست همروندی، بررسی وضعیت Flow و mocking جزئی اشیاء واقعی.
Spy (یا partial mock) امکان ایجاد شیءای را فراهم میکند که فراخوانیها را به پیادهسازی واقعی واگذار میکند، اما اجازه بازنویسی متدهای خاص را میدهد. در MockK، spyk بر اساس نمونه واقعی کلاس ایجاد میشود: val repo = spyk(InMemoryUserRepository()). فراخوانیهایی که انتظارات برای آنها از طریق every تعیین شده است از Mock عبور میکنند؛ بقیه — از شیء واقعی. Spy بهویژه برای تست کدهای قدیمی که تزریق وابستگی هنوز در آنها پیادهسازی نشده مفید است، زمانی که فقط باید یک متد را بازنویسی کرد.
در پروژههای مدرن Android با Jetpack Compose، ViewModel وضعیت را از طریق StateFlow آشکار میکند. MockK امکان mock کردن وابستگیهای Flow را فراهم میکند و کتابخانه Turbine بررسی انتشار را ساده میکند. الگوی کلاسیک: MockK برای UseCase که Flow برمیگرداند، Turbine برای بررسی انتشار ViewModel. این پشته توسط مستندات Android Testing (Google, 2024) برای پروژههای مبتنی بر Kotlin Coroutines توصیه میشود.
class SearchViewModelTest {
private val searchUseCase = mockk<SearchUseCase>()
private val vm = SearchViewModel(searchUseCase)
fun `search emits results`() = runTest {
coEvery { searchUseCase.search("android") } returns
flowOf(SearchResult.Success(listOf(Item("Android TDD"))))
vm.search("android")
vm.state.test {
val state = awaitItem()
assertTrue(state.items.isNotEmpty())
cancelAndIgnoreRemainingEvents()
}
}
}
سوالات متداول
Mock — یک مفهوم، نوع Test Double بررسیکننده رفتار است. Mockito — کتابخانهای برای ایجاد اشیاء Mock در Java و Android است. کتابخانههای دیگر: MockK (Kotlin), EasyMock (Java), Cuckoo (iOS).
برای تست توابع suspend با Mock از MockK (coEvery / coVerify) یا Mockito با mockito-kotlin استفاده کنید. MockK از کوروتینها بهطور بومی پشتیبانی میکند: coEvery رفتار تابع suspend را تعیین میکند، coVerify فراخوانی آن را درون کوروتین بررسی میکند. تمام فراخوانیهای suspend باید درون runTest (kotlinx-coroutines-test) اجرا شوند.
بله. در MockK برای این کار از returnsMany استفاده میشود: every { api.getData() } returnsMany listOf(response1, response2). در Mockito — زنجیره thenReturn(value1).thenReturn(value2). این برای تست رفتار در فراخوانیهای متوالی با پاسخهای متفاوت مفید است.
در MockK از annotation @MockK با فیلد relaxed = true استفاده کنید و در متد @After clearMocks(mock) را فراخوانی کنید. در Mockito — Mockito.reset(mock). بهترین روش: برای هر تست یک Mock جدید از طریق @Before ایجاد کنید تا تأثیر بین تستها حذف شود.
MockK به درستی با sealed class کار میکند: every { useCase() } returns Result.Success(data). Mockito مستقیماً از sealed class پشتیبانی نمیکند و نیاز به روشهای جایگزین دارد. این یکی از دلایلی است که برای پروژههای Kotlin به جای Mockito، MockK توصیه میشود.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید