Test Doubles — این شیءهای جایگزینی هستند که در تستهای واحد به جای وابستگیهای واقعی استفاده میشوند. این اصطلاح توسط Gerard Meszaros در کتاب «xUnit Test Patterns» (2007) به عنوان یک مفهوم کلی برای Mock، Stub، Fake، Spy و Dummy معرفی شد. به گزارش مارتین فاولر (2024)، Test Doubles به جداسازی کامپوننت مورد آزمایش از محیط آن کمک میکنند و آزمایشها را تعیینگرا، سریع و مستقل از خدمات خارجی میسازند.
نکات کلیدی
Test Doubles — این اصطلاحی از صنعت خودروسازی (بدلکار، «جایگزین» برای بازیگران) است که به توسعه نرمافزار منتقل شده است. همانطور که یک بدلکار بازیگر را در یک ساحنه خطرناک جایگزین میکند، Test Double یک کامپوننت واقعی را در یک سناریو آزمایشی جایگزین میکند. این زمانی ضروری است که وابستگی واقعی در دسترس نباشد، کند باشد، غیرتعیینگرا باشد یا عوارض جانبی داشته باشد.
مفهوم Test Double شامل پنج نوع مشخص است که هر کدام مسئله خود را حل میکند. طبقهبندی Meszaros کانونی است و در تمامی راهنماهای مدرن آزمایش استفاده میشود. تفاوت بین انواع در میزان کنترل و بازبینی است: از پر کردن ساده پارامترها (Dummy) تا بررسی کامل تسلسل فراخوانیها (Mock).
هدف اصلی Test Doubles جداسازی ماژول مورد آزمایش است. در توسعه موبایل، وابستگیهای واقعی شامل سرورهای API، پایگاههای داده، سیستم پرونده، حسگرهای دستگاه، خدمات سیستمی (LocationManager، Camera، Bluetooth) هستند. استفاده مستقیم از این کامپوننتها آزمایشها را کند، شکنانده و وابسته به محیط میکند. به گزارش Google Testing Blog (2023)، آزمایشهای واحد بهخوبی جداشده شده در طول میلیثانیه انجام میشوند، و آزمایشهای انتگرالیونی در ثانیه و دقیقه.
طبقهبندی Gerard Meszaros شامل پنج نوع Test Doubles است که از نظر رفتار و هدف استفاده تفاوت دارند. درک تفاوت بین آنها اساس آزمایش واحد صحیح است.
Dummy — شیءی است که به متد مورد آزمایش ارسال میشود اما هرگز استفاده نمیشود. Dummy فقط برای برآوردن امضای متد مورد نیاز است. در Kotlin این عموماً null، emptyList() یا یک شیء با جایگزینهاست. Dummy نباید هیچ منطقی داشته باشد — اگر فراخوانده شود، آزمایش باید شکست بخورد.
Fake — یک پیادهسازی ساده شده اما کاربردی از یک رابط است. بر خلاف Mock و Stub، Fake شامل منطق کسب وکار واقعی است، اما به صورت ساده شده. مثال کلاسیک — InMemoryUserRepository که دادهها را به جای پایگاه داده در HashMap ذخیره میکند. Fake زمانی استفاده میشود که منطق وابسته به وضعیت بدون سربار زیرساخت واقعی آزمایش شود.
| نوع | کاربرد | مثال |
|---|---|---|
| Dummy | پر کردن پارامتر | null، شیء خالی |
| Fake | پیادهسازی ساده شده کاربردی | InMemoryRepository |
| Stub | بازگرداندن مقدار ثابت | when(api.getUser()).thenReturn(user) |
| Spy | ثبت فراخوانیها برای بررسی | verify(spy).save(user) |
| Mock | بررسی تعامل | verify(mock).sendEmail(email) |
Stub مقادیر از پیش تعیین شده را برای فراخوانیهای مشخص بازمیگرداند. Stub بررسی نمیکند که آیا فراخوانده شده است — آن فقط داده توسیله میکند. در Mockito، Stub از طریق when(method).thenReturn(value) ایجاد میشود. Stub برای آزمایش زمانی ایدهآل است که وابستگی باید مقدار مشخصی را بازگرداند، اما واقعیت فراخوانی مهم نیست.
Spy — یک پیچش در اطراف یک شیء واقعی است که تمامی فراخوانیها را برای بازبینی بعدی ثبت میکند. بر خلاف Mock، Spy فراخوانیها را به شیء واقعی ارجاع میدهد، اما امکان بررسی وقوع آنها را فراهم میکند. در Mockito، Spy از طریق spy(realObject) ایجاد میشود. Spy برای mocking جزئی مفید است زمانی که میخواهید از یک شیء واقعی استفاده کنید اما برخی فراخوانیها را بررسی کنید.
Mock — یک شیء با چشمانتظارات فراخوانی از پیش تعریف شده است. Mock بررسی میکند که آیا متدهای مشخصی با آرگمانهای مشخصی و در ترتیب مشخصی فراخوانده شدهاند. بر خلاف Stub، Mock بر بازبینی رفتار تمرکز دارد، نه بر بازگرداندن داده. Mock قویترین و پراستفادهترین نوع Test Double در توسعه موبایل است.
تفاوت بین Mock و Stub حتی در بین توسعهدهندگان مبتدی نیز سترگی ایجاد میکند. تفاوت اصلی در هدف است: Stub وضعیت را بررسی میکند (state verification)، Mock رفتار را بررسی میکند (behavior verification).
Stub به این سؤال پاسخ میدهد: «آیا کد نتیجه صحیحی را بازگرداند؟». Mock به این سؤال پاسخ میدهد: «آیا کد روشهای مناسب را با آرگمانهای مناسب فراخواند؟». در توسعه موبایل، Stub زمانی استفاده میشود که نتیجه مهم است (مثلاً، داده از رپوزیتوری)، و Mock زمانی که عوارض جانبی مهم هستند (مثلاً، ارسال ایمیل، نوشتن در پایگاه داده).
// Stub: بررسی وضعیت
every { repository.getUsers() } returns listOf(user)
val result = useCase.getUsers()
assertEquals(1, result.size)
// Mock: بررسی رفتار
every { analytics.logEvent("purchase") } returns Unit
useCase.purchase(item)
verify { analytics.logEvent("purchase") }
مثالهای عملی هر پنج نوع Test Doubles در Kotlin با استفاده از MockK — محبوبترین کتاخانه mocking برای پروژههای Android.
class InMemoryUserRepository : UserRepository {
private val store = mutableMapOf<String, User>()
override fun save(user: User) {
store[user.email] = user
}
override fun findByEmail(email: String): User? {
return store[email]
}
}
class RegisterUseCaseTest {
private val api = mockk<AuthApi>()
private val repo = spyk(InMemoryUserRepository())
private val useCase = RegisterUseCase(api, repo)
fun `register user successfully`() = runTest {
// Stub: بازگرداندن پاسخ ثابت API
coEvery { api.register("test@test.com") } returns AuthResult.Success("token123")
val result = useCase.execute("test@test.com")
// Verify: بررسی که کاربر ذخیره شده است
verify { repo.save(any()) }
assertTrue(result.isSuccess())
}
}
data class Logger(val appContext: Context, val format: FormatType)
fun `test logger with dummy context`() {
// Dummy: Context در داخل Logger استفاده نشده است
val dummyContext = mockk<Context>()
val logger = Logger(dummyContext, FormatType.JSON)
assertEquals(FormatType.JSON, logger.format)
}
انتخاب نوع Test Double بستگی به این دارد که دقیقاً چه چیزی آزمایش میشود: وضعیت، رفتار یا انتگرالیون. در توسعه موبایل بر روی Android و iOS توصیههای زیر شکل گرفتهاند.
در آزمایش ViewModel، از Mock برای وابستگیهایی که عوارض جانبی ایجاد میکنند (رپوزیتوریها، تحلیل، ناوبری) و از Stub برای وابستگیهایی که داده بازمیگردانند (مشتریان API، ContentProvider) استفاده کنید. این امکان را فراهم میکند که ViewModel هم سناریوهای موفق و هم سناریوهای خطا را به صورت صحیح مدیریت کند.
در سطح Repository، Fake (پیادهسازیهای پایگاه داده in-memory) و Stub (پاسخهای ثابت API) ترجیح دارند. Fake امکان آزمایش منطق کش و حافظه موقت و حالت آفلاین را بدون تنظیمات SQLite فراهم میکند. Stub کدهای مختلف HTTP را شبیهسازی میکند: 200، 404، 500، timeout.
استفاده نادرست از Test Doubles — یکی از شایعترین علتهای آزمایشهای شکنانده است که در هر بازنویسی میشکنند.
شایعترین اشتباه — mocking هر چیز. اگر هر وابستگی در آزمایش با Mock جایگزین شود، آزمایش از بررسی رفتار واقعی باز میایستد. Mock فقط برای وابستگیهای خارجی (شبکه، پایگاه داده، سیستم پرونده، خدمات سیستمی) باید باشد. کامپوننتهای داخلی برنامه (Value Object، data class، ابزارهای ساده) نباید جایگزین شوند.
دومین اشتباه — ایجاد Mock بدون تعریف چشمانتظارات. اگر متدی بدون every / when فراخوانده شود، Mock مقدار پیشفرض را بازمیگرداند (null، 0، false). این میتواند به آزمایشهای مثبت کاذب منجر شود زمانی که Mock ساکت null را بازمیگرداند و آزمایش این را به عنوان رفتار صحیح تفسیر کند.
سومین اشتباه — بررسی هر فراخوانی هر Mock. Verify فقط برای فراخوانیهایی استفاده شود که از نظر منطق کسب وکار حیاتی هستند. بازبینی از حد آزمایشها را شکنانده میکند: تغییر ترتیب فراخوانیها در کد تولید بدون تغییر رفتار آزمایشها را میشکند.
پرسشهای متداول
Stub دادهها را بازمیگرداند و وضعیت را بررسی میکند (چه چیزی بازگشته شد،) و Mock رفتار را بررسی میکند (چه متدهایی فراخوانده شدند). Stub = «X را بازگردان»، Mock = «بررسی کن که Y با آرگمان Z فراخوانده شد». در آزمایشهای واقعی، یک شیء چندان هم همچون Stub و هم همچون Mock عمل میکند.
Fake به Mock ترجیح دارد زمانی که منطق وابسته به وضعیت آزمایش میشود: کش، حالت آفلاین، ترانزاکشنها. Fake (پیادهسازی in-memory) امکان آزمایش این سناریوها را بدون فراخوانیهای verify شکنانده فراهم میکند. Mock برای بررسی ارسال داده مناسبتر است: تحلیل، اعلام پش، ایمیل.
برای پروژههای Android در Kotlin، MockK توصیه میشود. آن از کوروتینها، تابعهای suspend، sealed class و تابعهای extension را بدون تنظیمات اضافی پشتیبانی میکند. برای پروژههای Java، استاندارد هنوز Mockito است — محبوبترین کتاخانه با مستندات گسترده.
برای آزمایش Kotlin Flow، از کتاخانه Turbine در کنار MockK استفاده کنید. Turbine بررسی تولید Flow را ساده میکند: میتوان ترتیب مقادیر، پایان جریان و استثناها را بررسی کرد. Stub برای Flow flowOf(value) را بازمیگرداند، Mock بررسی میکند که Flow جمعآوری شده است.
بله، اما در سطح پاسخهای API، نه کامپوننتهای UI. کتاخانههای MockWebServer (OkHttp) و WireMock امکان جایگزینی پاسخهای HTTP را در آزمایشهای UI فراهم میکنند. کامپوننتهای UI (Compose، SwiftUI Views) نباید جایگزین شوند — رفتار آنها از طریق آزمایشهای screenshot و Espresso آزمایش میشود.
نتایج
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید