تست واحد در توسعه موبایل: چیست، روش‌ها و فریمورک‌ها

نویسنده: IT Sectr منتشر شده: 2026-04-06 زمان مطالعه: 9 دقیقه

تست واحد روشی برای بررسی نرم‌افزار است که در آن صحت عملکرد ماژول‌ها یا توابع کد به‌صورت مجزا از بقیه سیستم آزمایش می‌شود. بر اساس Martin Fowler, 2026، تست‌های واحد پایه CI/CD و بازسازی کد (refactoring) هستند و بازخورد سریعی درباره عملکرد صحیح کد فراهم می‌کنند. تست ماژولار به یافتن خطاها در مراحل اولیه توسعه کمک می‌کند و هزینه رفع آن‌ها را تا ده‌ها برابر کاهش می‌دهد.

نکات کلیدی

  • تست واحد — بررسی یک ماژول (تابع، متد، کلاس) به‌صورت مجزا از وابستگی‌های خارجی
  • اصول FIRST — Fast, Isolated, Repeatable, Self-validating, Timely — پایه تست‌های باکیفیت
  • مک‌ها و استاب‌ها — جایگزین‌های وابستگی‌های خارجی (پایگاه داده، API، سیستم فایل) که ایزولاسیون تست را تضمین می‌کنند
  • TDD (Test-Driven Development) — متدولوژی توسعه از طریق تست‌نویسی: قرمز-سبز-بازسازی
  • هرم تست — تست‌های واحد ۷۰٪ هرم را تشکیل می‌دهند و بازخورد سریعی در هر commit ارائه می‌کنند

تست واحد چیست؟

تست واحد فرآیندی برای بررسی واحدهای مجزا (unit) از کد منبع — توابع، متدها، کلاس‌ها — به‌صورت مستقل از بقیه برنامه است. هر تست یک سناریوی مشخص از استفاده از ماژول را اجرا می‌کند و بررسی می‌کند که نتیجه با انتظارات مطابقت دارد. تست‌های واحد به همان زبان برنامه‌نویسی کد اصلی نوشته می‌شوند و به‌صورت خودکار در محیط توسعه یا در خط لوله CI/CD اجرا می‌شوند. برخلاف تست‌های یکپارچه‌سازی، تست‌های واحد با پایگاه‌های داده واقعی، سیستم فایل یا سرویس‌های شبکه‌ای تعامل ندارند.

چرا تست‌های واحد لازم هستند؟

هدف اصلی بازخورد سریع درباره صحت کد پس از تغییرات است. اگر توسعه‌دهنده یک متد را بازسازی (refactor) کند، مجموعه تست‌های واحد تأیید می‌کند که رفتار کد خراب نشده است. بر اساس Google Testing Blog (2025)، پروژه‌هایی با پوشش تست واحد بالای ۶۰٪، ۲.۵ برابر حوادث تولید (production) کمتری دارند. مزایای دیگر: مستندسازی کد (تست‌ها نشان می‌دهند که چگونه از API استفاده کنید)، ساده‌سازی بازسازی (می‌توانید پیاده‌سازی را تغییر دهید و رفتار را حفظ کنید) و تشخیص سریع رگرسیون‌ها.

چه چیزی تست واحد محسوب می‌شود؟

هر تست خودکار یک تست واحد نیست. معیارها: یک ماژول (کلاس یا تابع) آزمایش می‌شود، وابستگی‌های خارجی با مک یا استاب جایگزین شده‌اند، تست در چند میلی‌ثانیه اجرا می‌شود و نیازی به بالا آوردن سرور یا پایگاه داده ندارد. تستی که به پایگاه داده واقعی دسترسی دارد — تست یکپارچه‌سازی است. تستی که مرورگر را باز می‌کند — تست E2E است. درک مرزهای بین انواع تست‌ها برای توزیع صحیح تلاش‌ها در هرم تست اهمیت دارد.

اصول FIRST و ساختار AAA

تست‌های واحد باکیفیت از اصول FIRST پیروی می‌کنند که توسط Robert C. Martin تدوین شده است. هر تست باید Fast (سریع — چند میلی‌ثانیه)، Isolated (منزوی — به تست‌های دیگر وابسته نیست)، Repeatable (تکرارپذیر — نتیجه یکسان روی هر ماشین)، Self-validating (خودسنج — نتیجه «passed» یا «failed» بدون بررسی دستی) و Timely (به‌موقع — قبل یا همزمان با کد نوشته می‌شود) باشد. نقض هر یک از این اصول ارزش تست را کاهش می‌دهد.

ساختار AAA (Arrange-Act-Assert)

الگوی استاندارد برای نوشتن تست‌های واحد. Arrange — آماده‌سازی داده‌ها و وابستگی‌ها: ایجاد اشیا، پیکربندی مک‌ها، تعیین پارامترهای ورودی. Act — اجرای عمل مورد آزمایش: فراخوانی متد یا تابع. Assert — بررسی نتیجه: مقایسه مقدار واقعی با مقدار مورد انتظار. تقسیم به سه بخش، تست را خوانا و قابل‌درک می‌کند. اگر بخش Assert نیاز به منطق پیچیده داشته باشد، احتمالاً تست بیش از حد در یک زمان بررسی می‌کند.

kotlin
// نمونه تست واحد بر اساس الگوی AAA در Kotlin با JUnit 5
class CalculatorTest {

    private lateinit var calculator: Calculator

    @BeforeEach
    fun setUp() {
        // ARRANGE — شیء مورد آزمایش را می‌سازیم
        calculator = Calculator()
    }

    @Test
    fun addition_shouldReturnCorrectSum() {
        // ACT — عمل را اجرا می‌کنیم
        val result = calculator.add(2, 3)

        // ASSERT — نتیجه را بررسی می‌کنیم
        Assertions.assertEquals(5, result)
    }
}

نام‌گذاری تست‌ها

نام تست باید توصیف کند که چه چیزی بررسی می‌شود و چه نتیجه‌ای انتظار می‌رود. قالب: [methodName]_[scenario]_[expectedResult]. مثال: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. نام خوب تست جایگزین کامنت می‌شود و هنگام شکست بلافاصله نشان می‌دهد کدام قابلیت خراب شده است. از نام‌هایی مانند test1، checkSomething یا verify اجتناب کنید — آن‌ها اطلاعاتی ندارند و تشخیص را دشوار می‌کنند.

مک‌ها، استاب‌ها و فیک‌ها: چه چیزی و چه زمانی استفاده شود

برای جداسازی ماژول مورد آزمایش از وابستگی‌های خارجی از دوبلورهای تستی (test doubles) استفاده می‌شود. انواع اصلی: مک‌ها (mocks) — بررسی می‌کنند که یک متد مشخص با پارامترهای درست فراخوانی شده است؛ استاب‌ها (stubs) — هنگام فراخوانی متد، مقادیر از پیش تعیین‌شده را برمی‌گردانند؛ فیک‌ها (fakes) — پیاده‌سازی ساده‌شده‌ای از یک کامپوننت واقعی (مثلاً InMemoryUserRepository به‌جای UserRepository که با پایگاه داده کار می‌کند). انتخاب بستگی به این دارد که چه چیزی را می‌خواهید بررسی کنید: حالت (state) (استاب) یا تعامل (مک).

دوبلورچه چیزی را بررسی می‌کندمثال
مکفراخوانی متد با پارامترهای صحیحuserRepository.save(user) دقیقاً ۱ بار فراخوانی شد
استابمقدار بازگشتیrepository.findById(1) مقدار User(id=1, name="Test") را برمی‌گرداند
فیکمنطق از طریق پیاده‌سازی ساده‌شدهInMemoryMapUserRepository با HashMap به‌جای پایگاه داده
جاسوسمک کردن جزئی شیء واقعیspy(repo).when(findById).thenReturn(user)

Mockito: نمونه مک کردن در Java/Kotlin

Mockito محبوب‌ترین فریمورک مک کردن در Java و Kotlin است. این امکان را می‌دهد که با mock() مک بسازید، با when().thenReturn() مقدار بازگشتی را تنظیم کنید و با verify() فراخوانی‌ها را بررسی کنید. نسخه‌های مدرن Mockito (5.x) از مک‌های استاتیک (mockStatic) و سینتکس ساده‌شده از طریق BDDMockito (given-willReturn) پشتیبانی می‌کنند. قانون مهم: چیزی را که مال شما نیست مک نکنید — برای value-objectها و کتابخانه‌های استاندارد مک نسازید.

kotlin
// نمونه تست واحد با Mockito در Kotlin
class OrderServiceTest {

    @Mock
    private lateinit var paymentGateway: PaymentGateway

    @Mock
    private lateinit var userRepository: UserRepository

    private lateinit var orderService: OrderService

    @BeforeEach
    fun init() {
        MockitoAnnotations.openMocks(this)
        orderService = OrderService(paymentGateway, userRepository)
    }

    @Test
    fun processOrder_whenPaymentFails_shouldThrowException() {
        // given
        val user = User(id = 1, balance = 100.0)
        val order = Order(amount = 200.0)
        Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
        Mockito.`when`(userRepository.findById(1)).thenReturn(user)

        // when & then
        assert Throws<PaymentException> {
            orderService.processOrder(user.id, order)
        }

        // verify
        Mockito.verify(paymentGateway).charge(any())
    }
}

TDD: توسعه مبتنی بر تست

TDD (توسعه مبتنی بر تست) متدولوژی‌ای است که در آن تست قبل از پیاده‌سازی کد نوشته می‌شود. چرخه «Red-Green-Refactor»: تستی بنویسید که شکست می‌خورد (Red)، حداقل کد را برای عبور از تست بنویسید (Green)، کد را بدون تغییر رفتار بهبود دهید (Refactor). TDD تضمین می‌کند که تمام کد با تست پوشش داده شده است (پوشش = ۱۰۰٪ برای قابلیت نوشته‌شده) و کد قابل‌تست است — اگر تست کردن کد دشوار باشد، یعنی معماری نیاز به بهبود دارد.

مزایای TDD

بر اساس مطالعه IBM (2006-2026، مطالعه طولی)، تیم‌هایی که از TDD استفاده می‌کنند نسبت به تیم‌هایی که تست را بعد از کد می‌نویسند، ۴۰ تا ۸۰٪ نقص‌های کمتری در production دارند. TDD همچنین معماری را بهبود می‌بخشد: توسعه‌دهنده مجبور است قبل از پیاده‌سازی به طراحی API فکر کند که منجر به اتصال سست (loose coupling) و انسجام بالا (high cohesion) می‌شود. اثر اضافی — مستندسازی با کد زنده: تست‌ها به‌عنوان مشخصات رفتار ماژول عمل می‌کنند که همیشه به‌روز هستند.

چه زمانی TDD مناسب نیست؟

TDD همیشه بهینه نیست. تست کردن کامپوننت‌های UI به‌صورت مجزا دشوار است — برای آن‌ها تست‌های snapshot یا تست رگرسیون بصری (Percy, Chromatic) مؤثرتر هستند. نمونه‌سازی اولیه و پژوهش (spike solutions) به تست نیاز ندارند. کد legacy بدون تست از طریق TDD به‌سختی پوشش داده می‌شود — اینجا ابتدا به characterization tests (تست‌هایی که رفتار فعلی را قبل از بازسازی تثبیت می‌کنند) نیاز دارید. در این موارد TDD کاملاً کنار گذاشته نمی‌شود، بلکه تطبیق داده می‌شود — برای قابلیت‌های در حال تغییر تست نوشته می‌شود، نه برای کل کد legacy.

تست واحد در اپلیکیشن‌های موبایل

توسعه موبایل ویژگی خاص خود را دارد: منطق کسب‌وکار اغلب با کد UI ترکیب می‌شود (Activity, ViewController, ViewModel) که تست واحد را پیچیده می‌کند. بهترین روش — Viewهای نازک، ViewModelهای ضخیم: تمام منطق را از کامپوننت‌های UI به کلاس‌های جداگانه (UseCase, Repository, ViewModel) منتقل کنید که بدون ایمولاتور به‌راحتی قابل‌تست هستند. برای Android و iOS فریمورک‌های بومی تست واحد وجود دارند که روی JVM/Native بدون اجرای دستگاه کار می‌کنند.

تست‌های واحد در Android (JUnit + Mockito/Robolectric)

تست‌های واحد Android روی JVM محلی بدون ایمولاتور اجرا می‌شوند که سرعت اجرا را تضمین می‌کند — یک تست معمولی کمتر از < 100ms طول می‌کشد. JUnit 5 runner اصلی است. برای تست‌های ViewModel از kotlinx-coroutines-test برای تست کوروتین‌ها و Turbine برای تست StateFlow استفاده کنید. Robolectric امکان تست کامپوننت‌های وابسته به Android (Context, Resources) را بدون ایمولاتور فراهم می‌کند و کلاس‌های shadow را بارگذاری می‌کند. برای تست‌های Compose از Compose UI Test استفاده کنید — اما این‌ها تست‌های UI هستند، نه unit.

تست‌های واحد در iOS (XCTest + Quick/Nimble)

تست‌های واحد iOS با Swift و XCTest (که در Xcode تعبیه شده) نوشته می‌شوند. Quick + Nimble — فریمورک‌های BDD برای تست‌های خواناتر (describe/context/it). برای مک کردن از Cuckoo (تولید مک) یا SwiftyMocky استفاده کنید. Swift از پروتکل‌ها و تزریق وابستگی (dependency injection) پشتیبانی می‌کند که جایگزینی وابستگی‌ها را آسان می‌کند. نکته کلیدی: تست‌های واحد iOS روی شبیه‌ساز macOS اجرا می‌شوند، نه روی دستگاه واقعی. تست‌هایی که به قابلیت‌های سخت‌افزاری نیاز دارند (دوربین، بلوتوث) — تست‌های یکپارچه‌سازی هستند.

تست‌های واحد در Flutter (flutter_test + Mockito)

تست‌های واحد Flutter از پکیج flutter_test استفاده می‌کنند و روی Dart VM بدون ایمولاتور اجرا می‌شوند. برای مک کردن — پکیج mockito با تولیدکننده کد (build_runner). تست‌های Widget (در همان پکیج) ویجت‌های جداگانه را آزمایش می‌کنند، اما به رندر نیاز دارند و کندتر هستند — فقط برای بررسی منطق UI از آن‌ها استفاده کنید. منطق خالص Dart (مدل‌ها، ریپازیتوری‌ها، بلاب‌ها) به‌عنوان تست‌های Dart معمولی بدون import کردن flutter_test آزمایش می‌شود.

dart
// نمونه تست واحد در Flutter با mockito
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';

@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';

void main() {
    late MockApiClient mockApi;
    late UserRepository repository;

    setUp(() {
        mockApi = MockApiClient();
        repository = UserRepository(mockApi);
    });

    test('fetchUser returns user when API succeeds', () async {
        // Arrange
        final expectedUser = User(id: 1, name: 'Test');
        when(mockApi.getUser(1))
            .thenAnswer((_) async => expectedUser);

        // Act
        final result = await repository.fetchUser(1);

        // Assert
        expect(result, expectedUser);
        verify(mockApi.getUser(1)).called(1);
    });
}

بهترین روش‌ها و خطاهای رایج

تست واحد مؤثر نیازمند نظم و انضباط است. قانون اصلی: رفتار را تست کنید، نه پیاده‌سازی را. تست نباید بداند که ماژول در داخل چگونه پیاده‌سازی شده است (چه متدهای خصوصی‌ای در چه ترتیبی فراخوانی می‌شوند). اگر تست به پیاده‌سازی وابسته باشد، در هر بازسازی می‌شکند و ارزش خود را از دست می‌دهد. تست قرارداد را بررسی می‌کند: با ورودی X باید خروجی Y به دست آید. استثنا — تست‌های الگوریتم‌هایی با عملکرد حیاتی، جایی که ترتیب فراخوانی‌ها مهم است.

  • یک بررسی در هر تست — یک assert یا یک گروه از assertهای مرتبط برای یک بررسی منطقی
  • از تکرار اجتناب کنید — از @BeforeEach / setUp برای مقداردهی مشترک و از تست‌های پارامتری برای داده‌های ورودی مختلف استفاده کنید
  • متدهای خصوصی را تست نکنید — از طریق API عمومی تست کنید. اگر یک متد خصوصی پوشش داده نشده باشد، یعنی منطق آن از بیرون قابل مشاهده نیست
  • حالت‌های مرزی را پوشش دهید — مجموعه‌های خالی، null/undefined، اعداد منفی، مقادیر حداکثر
  • در تست‌ها از Thread.sleep استفاده نکنید — این کار تست‌ها را کند و ناپایدار می‌کند. از تایم‌اوت‌های تستی و کوروتین‌ها استفاده کنید

چه پوششی (coverage) کافی محسوب می‌شود؟

پوشش ۱۰۰٪ — هدفی دست‌نیافتنی و غیرضروری است. بر اساس Google Testing Blog (2025)، سطح بهینه پوشش برای تست‌های واحد — ۷۰ تا ۸۰٪ خطوط کد است. پوشش ۱۰۰٪ اغلب با تست getterها، setterها و سازنده‌ها به دست می‌آید که ارزشی ندارد. روی منطق حیاتی کسب‌وکار تمرکز کنید: محاسبات پیچیده، اعتبارسنجی، مدیریت خطا، حالت‌های مرزی (edge cases). برای اندازه‌گیری از JaCoCo (Java)، Coverage.py (Python)، Istanbul (JS) استفاده کنید و آستانه را در CI تنظیم کنید — شکست build در پوشش کمتر از ۶۰٪.

CI/CD و تست‌های واحد

تست‌های واحد — مرحله اول هر خط لوله CI/CD هستند. آن‌ها در هر push به مخزن، قبل از build و deploy اجرا می‌شوند. میانگین زمان اجرای تست‌های واحد در پروژه نباید از ۵ دقیقه بیشتر شود — اگر طولانی‌تر باشد، تست‌ها دیگر «سریع» نیستند و توسعه‌دهندگان دیگر آن‌ها را به‌صورت محلی اجرا نمی‌کنند. تست‌ها را به سریع (unit) و کند (یکپارچه‌سازی) تقسیم کنید و در استیج‌های مختلف خط لوله اجرا کنید. برای تسریع از parallel execution و fail-fast استفاده کنید.

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

تست واحد چه تفاوتی با تست یکپارچه‌سازی دارد؟

تست واحد یک ماژول را به‌صورت مجزا بررسی می‌کند و وابستگی‌های خارجی را با مک‌ها جایگزین می‌کند. تست یکپارچه‌سازی تعامل بین چندین کامپوننت واقعی (پایگاه داده، API، سیستم فایل) را بررسی می‌کند. تست‌های واحد در چند میلی‌ثانیه اجرا می‌شوند، تست‌های یکپارچه‌سازی — در چند ثانیه. در هرم تست، تست‌های واحد ۷۰٪ را تشکیل می‌دهند.

کدام فریمورک را برای تست‌های واحد انتخاب کنیم؟

انتخاب به پلتفرم بستگی دارد: JUnit 5 برای Java/Kotlin، XCTest برای iOS/Swift، pytest برای Python، Jest/Vitest برای JavaScript/TypeScript، flutter_test برای Flutter. برای مک کردن از Mockito (Java)، Cuckoo (iOS)، unittest.mock (Python) یا vitest.mock (JS) استفاده کنید. همه فریمورک‌های مدرن از تست‌های پارامتری، assertionهای داخلی و اجرای موازی پشتیبانی می‌کنند.

اصول F.I.R.S.T. تست چیست؟

Fast — تست در چند میلی‌ثانیه اجرا می‌شود. Isolated — به تست‌های دیگر و سیستم‌های خارجی وابسته نیست. Repeatable — روی هر ماشین نتیجه یکسان می‌دهد. Self-validating — نتیجه را به‌صورت خودکار بررسی می‌کند. Timely — قبل یا همزمان با کد نوشته می‌شود. نقض حتی یکی از اصول، اثربخشی تست را کاهش می‌دهد.

آیا باید برای ViewModel در Android/iOS تست واحد بنویسیم؟

بله، حتماً. ViewModel حاوی منطق کسب‌وکار است — پردازش رویدادها، تبدیل داده‌ها، مدیریت وضعیت. در Android از kotlinx-coroutines-test برای کوروتین‌ها و Turbine برای تست StateFlow استفاده کنید. در iOS Publisherهای Combine یا async/await را در ViewModel تست کنید. تست‌های ViewModel — تست‌های واحد خالص هستند که روی JVM/macOS بدون ایمولاتور اجرا می‌شوند.

چگونه کد با درخواست‌های شبکه‌ای را تست کنیم؟

درخواست‌های شبکه‌ای در تست‌های واحد اجرا نمی‌شوند — آن‌ها با مک‌های HTTP-کلاینت جایگزین می‌شوند. در Android از MockWebServer (OkHttp) استفاده کنید — یک سرور HTTP محلی راه‌اندازی می‌کند که از مک‌ها بهتر است، زیرا تعامل شبکه واقعی را بازتولید می‌کند. MockWebServer ایزولاسیون را بدون از دست دادن واقع‌گرایی فراهم می‌کند. برای iOS — OHHTTPStubs یا URLProtocol برای رهگیری و جایگزینی پاسخ‌ها.

جمع‌بندی

  • تست واحد — بررسی ماژول‌های مجزا به‌صورت مستقل از وابستگی‌های خارجی با بازخورد سریع
  • ساختار AAA — Arrange (آماده‌سازی)، Act (اقدام)، Assert (بررسی) — الگوی استاندارد تست
  • مک‌ها و استاب‌ها — دوبلورهای تستی برای ایزولاسیون: مک‌ها فراخوانی‌ها را بررسی می‌کنند، استاب‌ها مقادیر را برمی‌گردانند
  • TDD — توسعه مبتنی بر تست (Red-Green-Refactor) تعداد نقص‌ها را ۴۰ تا ۸۰٪ کاهش می‌دهد
  • اصول FIRST — Fast, Isolated, Repeatable, Self-validating, Timely — پایه تست باکیفیت
  • ابزارهای پلتفرمی — JUnit 5 (Android)، XCTest (iOS)، flutter_test (Flutter)، Jest (React Native)
  • پوشش ۷۰-۸۰٪ — سطح بهینه برای منطق حیاتی کسب‌وکار، getterها و setterها به تست نیاز ندارند

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید