MockK — یک چارچوب Kotlin-first برای ایجاد اشیاء mock است که بهطور ویژه برای اکوسیستم Kotlin با توجه به ویژگیهای زبانی آن طراحی شده است: کروتینها، توابع افزوده، data class و sealed class. به عکس Mockito که از Java به Kotlin پورت شده، MockK از ابتدا برای نحو نگارش Kotlin طراحی شده و برای کار با کلاسهای final به پلاگینهای اضافی نیاز ندارد. به طور میزان دادههای MockK.io، این کتابخانه در بیش از 40% پروژههای Kotlin با آزمونهای واحد استفاده میشود.
نکات کلیدی
MockK — یک کتابخانه برای ایجاد اشیاء mock است که به زبان Kotlin نوشته شده و برای نحو نگارش آن بهینهسازی شده است. همان مشکلاتی که Mockito حل میکند حل میکند — جداسازی کد تست شده از وابستگیها — اما این کار را با استفاده از ساختارهای ویژه Kotlin انجام میدهد: لامبداها، DSL، reified generics و توابع suspend.
مزیت اصلی MockK نسبت به راهحلهای پورت شده — پشتیبانی ذاتی از Kotlin است. در Mockito، mock کردن final-class نیازمند opt-in است (mockito-inline)، و روشهای ساکن نیازمند mockStatic هستند. MockK از این قابلیت بهصورت پیشفرض پشتیبانی میکند، زیرا کلاسهای Kotlin بهصورت پیشفرض final هستند و دور زدن این محدودیت در معماری کتابخانه ساخته شده است.
نسخه 1.13.12 (2024) — انتشار پایدارای است که از Kotlin 2.0، کامپایلر K2 و پروژههای چند سیستمی (KMP) پشتیبانی میکند. MockK نیز با Kotlin/Native و Kotlin/JS کار میکند، که آن را به تنها گزینه برای پروژههای KMP تبدیل میکند، جایی که نه Mockito و نه EasyMock قابل استفاده نیستند.
MockK با توجه به ویژگیهای Kotlin طراحی شده و از قابلیتهای زبان — reified generics، DSL با لامبداها، توابع inline — برای ارائه API مختصر و ایمن از نظر نوع بدون از دست دادن عملکرد استفاده میکند.
مکانیزم MockK بر اساس ابزاردهای بایتکد از طریق کتابخانه ByteBuddy استوار دارد (مانند Mockito)، اما آن را در چهارچوب یک DSL دوستانه Kotlin قرار میدهد. به جای زنجیرههای when().thenReturn()، MockK از بلوکهای لامبدایی every { } و coEvery { } استفاده میکند که به عنوان یک افزایش طبیعی زبان به نظر میرسند. در داخل، MockK فراخوانی داخل لامبدا را رهگیری میکند، روش و آرگومانها را از طریق بازتابی تجزیه و تحلیل میکند و آنها را با قوانین stubbing ثبت شده مطابقت میدهد.
بلوک every { mock.method() } returns value به این معنی است که «هر بار که روش فراخوانده شود، مقدار را برگردان». چنین نحو نگارش اعلامی به سبک Kotlin نزدیکتر است و سردرگمی با ترتیب آرگومانها در when() را از بین میبرد. به لطف reified generics Kotlin، نوع mock بدون تشخیص صریح کلاس بهصورت خوداتیک استنباط میشود.
val repository = mockk<UserRepository>()
// Stubbing: هر فراخوانی findById(1) کاربر را برمیگرداند
every { repository.findById(1) } returns User("Alice")
// فراخوانی و بررسی
val result = repository.findById(1)
assertEquals("Alice", result.name)
به عکس Mockito که هر روش را باید صریحاً پیکربندی کرد، MockK از relaxed mock پشتیبانی میکند — mockای که برای هر روش مقادیر پیشفرض «معقول» را برمیگرداند: لیست خالی برای List، 0 برای Int، رشته خالی برای String. این حجم کد آمادهسازی را به شدت کاهش میدهد.
// Relaxed mock — همه روشها مقادیر پیشفرض را برمیگردانند
val api = mockk<ApiService>(relaxed = true)
// نیازی به stubbing ندارد — لیست خالی را برمیگرداند
println(api.getUsers()) // []
MockK چندین روش برای ایجاد اشیاء mock ارائه میدهد: mockk<T>() برای mock سختگیرانه (هر روش باید صریحاً پیکربندی شود)، mockk<T>(relaxed = true) برای mock سبکشده و spyk(obj) برای ایجاد یک جاسوس بر روی شیء واقعی.
| تابع | نوع | رفتار بدون stubbing |
|---|---|---|
| mockk() | Mock سختگیرانه | در صورت فراخوانی روش stub-نشده استسنا میاندازد |
| mockk(relaxed = true) | Mock سبکشده | مقدار پیشفرض را برمیگرداند |
| spyk() | جاسوس | اگر stub پیکربندی نشده باشد، روش واقعی را فراخوان میکند |
| slot() | Argument Captor | آرگومان را برای بررسی ذخیره میکند |
انتخاب بین mock سختگیرانه و سبکشده به زمینه بستگی دارد. Mock سختگیرانه تضمین میکند که آزمون از روشهایی که رفتارشان تعریف نشده است، استفاده نمیکند — این قابلیت اطمینان را افزایش میدهد. Mock سبکشده برای نمونهسازی سریع آزمونها مناسب است جایی که همه وابستگیها مهم نیستند. در عمل، توصیه میشود از mock سختگیرانه شروع کرده و تنها زمانی به relaxed روی آورید که stubbing بیشتر از خود آزمون خط ها را اشغال کند.
بلوک every — ساختار مرکزی stubbing در MockK است. داخل لامبدا، فراخوانی روش با آرگومانهای مشخص توصیف میشود، و سپس مقدار از طریق returns برگردانده میشود، استسنا از طریق throws پرتاب میشود، یا پاسخ از طریق answers محاسبه میشود.
MockK از همه سناریوهای مورد نیاز جهت آزمون پشتیبانی میکند: بازگرداندن مقدار، پرتاب استسنا، محاسبه پاسخ بر اساس آرگومانها، چندین پاسخ به ترتیب (تسلسل فراخوانیها).
// بازگرداندن مقدار
every { repo.findById(1) } returns User("Alice")
// پرتاب استسنا
every { repo.findById(999) } throws NotFoundException()
// پاسخ پویا
every { repo.save(any()) } answers {
val user = firstArg<User>()
user.copy(id = 42)
}
// تسلسل پاسخها
every { repo.findAll() } returnsMany listOf(
listOf(User("Alice")),
listOf(User("Bob")),
emptyList()
)
Verify در MockK از نظر مفهوم مشابه Mockito.verify() است، اما از DSL Kotlin استفاده میکند: verify { mock.method() }. برای توابع suspend از coVerify { mock.suspendMethod() } استفاده میشود که با کروتینها به درستی کار میکند و نیازی به runner ویژه ندارد.
MockK از همان مودیفکاتورهایی که Mockito پشتیبانی میکند پشتیبانی میکند: exactly(1)، atLeast(2)، atMost(5)، wasNot(Called). نحو نگارش مینیمالیست — مودیفکاتور به عنوان اولین آرگومان در verify { } ارسال میشود.
// بررسی: روش دقیقاً 1 بار فراخوانده شد
verify(exactly = 1) { repo.save(any()) }
// بررسی ترتیب فراخوانیها
verifySequence {
repo.save(any())
repo.flush()
}
// coVerify برای توابع suspend
coVerify { api.fetchUsers() }
برای بررسی آرگومانها از slot() — معادل ArgumentCaptor استفاده میشود. Slot قبل از فراخوانی اعلام میشود، به every یا verify ارسال میشود، و پس از اجرای آزمون شامل مقدار ذخیره شده است.
val userSlot = slot<User>()
verify { repo.save(capture(userSlot)) }
assertEquals("Alice", userSlot.captured.name)
MockK انواتاسیونهای @MockK و @RelaxedMockK را برای ایجاد mock از طریق مقداردهی در JUnit 5 فراهم میکند. افزونه MockKExtension بهطور خودکار قبل از هر آزمون mock میسازد و پس از آن پاک میکند — مشابه MockitoExtension، اما با پشتیبانی از حالت relaxed.
انواتاسیون @InjectMockKs (یا جایگزین @MockK با ایجاد صریح شیء) mockها را به نمونه مورد آزمون تزریق میکند. این کدهای boilerplate را کاهش میدهد و کد آزمون را تمیزتر میکند.
@ExtendWith(MockKExtension::class)
class UserServiceTest {
@MockK
lateinit var repository: UserRepository
@InjectMockKs
lateinit var service: UserService
@Test
fun `getUser returns user from repository`() {
every { repository.findById(1) } returns User("Alice")
assertEquals("Alice", service.getUser(1)?.name)
}
}
انتخاب بین MockK و Mockito به ترکیب تیم و نوع پروژه بستگی دارد. Mockito اکوسیستم بزرگتری، نمونههای بیشتر و ادغامات بیشتری دارد، اما MockK نحو نگارش تمیزتر Kotlin و پشتیبانی ذاتی از ویژگیهای زبان را فراهم میکند. برای پروژههای جدید Kotlin، MockK به عنوان راهحل ایدیوماتیکتر توصیه میشود.
| معیار | MockK | Mockito |
|---|---|---|
| نحو نگارش | DSL Kotlin (every, verify) | سبک Java (when, thenReturn) |
| کروتینها | coEvery, coVerify (ذاتی) | نیازمند کتابخانههای اضافی |
| Final class | بهصورت پیشفرض پشتیبانی میشود | نیازمند mockito-inline |
| KMP | پشتیبانی میشود | پشتیبانی نمیشود |
| Relaxed mock | داخلی | هیچ معادلی ندارد |
| محبوبیت | در جامعه Kotlin در حال رشد | در Java و پروژههای ترکیبی دومینه دارد |
برای پروژههای Kotlin خالص (بدون کلاسهای Java) MockK تریجیح دارد: boilerplate کمتر، پشتیبانی ذاتی کروتین، هیچ شگفتی با کلاسهای final. برای پروژههای ترکیبی یا تیمهایی با سابقه Java، Mockito یک گزینه کاربردی باقی میماند — هر دو کتابخانه را میتوان در یک پروژه از طریق ماژولهای مختلف استفاده کرد. هنگام انتقال از Mockito به MockK، کافی است انواتاسیونهای @Mock را با @MockK جایگزین کرده و بلوکهای when().thenReturn() را به فرمت every { } تبدیل کرد.
سوالات متداول
Relaxed mock برای همه روشهای stub-نشده مقادیر پیشفرض را برمیگرداند (لیست خالی، 0، null)، بدون اینکه استسنا بفرستد. mock عادی (strict) نیازمند stubbing صریح هر روش است — در غیر این صورت آزمون شکست میخورد. Relaxed mock برای آزمونهای سریع مناسب است، strict برای آزمونهای مطمئن.
MockK از mock کردن توابع افزوده از طریق mockkStatic() پشتیبانی میکند. این امکانپذیر است زیرا توابع افزوده در Kotlin روشهای ساکنی هستند که پارامتر اول آنها دریافتکننده است. برای هر تابع افزوده، کلاسی که در آن اعلام شده است باید مشخص شود.
بله، MockK از Kotlin Multiplatform (KMP) برای کد مشترک پشتیبانی میکند. در سطح سایتهای JVM، Native و JS میتوان از API مشترک mockk()، every، verify استفاده کرد. این MockK را به تنها گزینه برای پروژههای KMP تبدیل میکند که Mockito در آنها کار نمیکند.
از verifySequence { } استفاده کنید — بلوکی که در آن فراخوانیها به طور سختگیرانه در ترتیب مورد انتظار مشخص شدهاند. اگر ترتیب واقعی متفاوت باشد، verifySequence با نشان دادن اولین فراخوانی نامطابق استسنا میافکند.
بله، از نظر فنی ممکن است، اما توصیه نمیشود. تعارضات میتواند در سطح ابزاردهای بایتکد ایجاد شود (ByteBuddy vs mockito-inline). اگر پروژه از قبل Mockito استفاده میکند، انتقال به MockK میتواند از طریق جداسازی ماژولها تدریجی انجام شود.
نتایج
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید