مقدمه ای بر تست دوتایی و تزریق وابستگی

این کد لبه بخشی از دوره Advanced Android in Kotlin است. اگر از طریق کدها به ترتیب کار کنید، بیشترین ارزش را از این دوره خواهید گرفت، اما اجباری نیست. همه کدهای دوره در صفحه فرود Android Advanced in Kotlin Codelabs فهرست شده اند.

مقدمه

این دومین کد لبه آزمایشی همه چیز در مورد آزمایش دوگانه است: زمان استفاده از آنها در اندروید، و نحوه پیاده سازی آنها با استفاده از تزریق وابستگی، الگوی یاب سرویس و کتابخانه ها. با انجام این کار، یاد می گیرید که چگونه بنویسید:

  • تست های واحد مخزن
  • تست های ادغام قطعات و viewmodel
  • تست های ناوبری قطعه

آنچه از قبل باید بدانید

باید با:

چیزی که یاد خواهید گرفت

  • نحوه برنامه ریزی استراتژی تست
  • نحوه ایجاد و استفاده از دوبل های آزمایشی، یعنی تقلبی و ساختگی
  • نحوه استفاده از تزریق وابستگی دستی در اندروید برای تست های واحد و ادغام
  • نحوه اعمال الگوی یاب سرویس
  • نحوه آزمایش مخازن، قطعات، مدل‌های مشاهده و مؤلفه Navigation

شما از کتابخانه ها و مفاهیم کد زیر استفاده خواهید کرد:

کاری که خواهی کرد

  • تست های واحد را برای یک مخزن با استفاده از تزریق دوتایی و وابستگی بنویسید.
  • تست های واحد را برای یک مدل view با استفاده از دوتایی تست و تزریق وابستگی بنویسید.
  • با استفاده از چارچوب تست UI Espresso، تست های یکپارچه سازی قطعات و مدل های نمای آنها را بنویسید.
  • تست های ناوبری را با استفاده از موکیتو و اسپرسو بنویسید.

در این سری از کدها، شما با برنامه TO-DO Notes کار خواهید کرد. این برنامه به شما امکان می دهد وظایف را برای تکمیل بنویسید و آنها را در یک لیست نمایش دهید. سپس می‌توانید آن‌ها را به‌عنوان تکمیل‌شده یا خیر علامت‌گذاری کنید، فیلتر کنید یا حذف کنید.

این برنامه به زبان Kotlin نوشته شده است، دارای چند صفحه نمایش است، از اجزای Jetpack استفاده می کند و معماری را از یک راهنما به معماری برنامه دنبال می کند. با یادگیری نحوه آزمایش این برنامه، می توانید برنامه هایی را که از کتابخانه ها و معماری مشابهی استفاده می کنند، آزمایش کنید.

کد را دانلود کنید

برای شروع، کد را دانلود کنید:

زیپ را دانلود کنید

از طرف دیگر، می توانید مخزن Github را برای کد کلون کنید:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_1

با پیروی از دستورالعمل‌های زیر، کمی وقت بگذارید و با کد آشنا شوید.

مرحله 1: برنامه نمونه را اجرا کنید

هنگامی که برنامه TO-DO را دانلود کردید، آن را در Android Studio باز کرده و اجرا کنید. باید کامپایل شود. با انجام موارد زیر برنامه را کاوش کنید:

  • با دکمه اکشن شناور پلاس یک کار جدید ایجاد کنید. ابتدا عنوانی را وارد کنید، سپس اطلاعات تکمیلی مربوط به کار را وارد کنید. آن را با چک سبز FAB ذخیره کنید.
  • در لیست کارها، روی عنوان کاری که به تازگی تکمیل کردید کلیک کنید و به صفحه جزئیات آن کار نگاه کنید تا بقیه توضیحات را ببینید.
  • در فهرست یا در صفحه جزئیات، کادر تأیید آن کار را علامت بزنید تا وضعیت آن را روی «تکمیل» تنظیم کنید.
  • به صفحه وظایف برگردید، منوی فیلتر را باز کنید و وظایف را بر اساس وضعیت فعال و تکمیل شده فیلتر کنید.
  • کشوی پیمایش را باز کنید و روی Statistics کلیک کنید.
  • به صفحه نمای کلی بازگشته و از منوی کشوی پیمایش، پاک کردن تکمیل شده را انتخاب کنید تا همه وظایف با وضعیت تکمیل شده حذف شوند.

مرحله 2: نمونه کد برنامه را کاوش کنید

برنامه TO-DO مبتنی بر نمونه آزمایشی و معماری محبوب طرح‌های معماری (با استفاده از نسخه معماری واکنشی نمونه) است. این برنامه از معماری یک راهنما به معماری برنامه پیروی می کند. از ViewModels با Fragments، Repository و Room استفاده می کند. اگر با هر یک از نمونه های زیر آشنا هستید، این برنامه معماری مشابهی دارد:

مهم‌تر است که معماری کلی برنامه را درک کنید تا اینکه درک عمیقی از منطق در هر لایه داشته باشید.

در اینجا خلاصه بسته هایی است که خواهید یافت:

بسته: com.example.android.architecture.blueprints.todoapp

.addedittask

صفحه افزودن یا ویرایش یک کار: کد لایه رابط کاربری برای افزودن یا ویرایش یک کار.

.data

لایه داده: این لایه با لایه داده وظایف سروکار دارد. این شامل پایگاه داده، شبکه و کد مخزن است.

.statistics

صفحه آمار: کد لایه رابط کاربری برای صفحه آمار.

.taskdetail

صفحه جزئیات کار: کد لایه رابط کاربری برای یک کار واحد.

.tasks

صفحه وظایف: کد لایه رابط کاربری برای لیست تمام وظایف.

.util

کلاس‌های کاربردی: کلاس‌های مشترک مورد استفاده در بخش‌های مختلف برنامه، به‌عنوان مثال برای طرح‌بندی بازخوانی کشیدن انگشت که در چندین صفحه استفاده می‌شود.

لایه داده (.data)

این برنامه شامل یک لایه شبکه شبیه سازی شده، در بسته راه دور ، و یک لایه پایگاه داده، در بسته محلی است. برای سادگی، در این پروژه لایه شبکه تنها با یک HashMap با تاخیر شبیه سازی می شود تا درخواست های واقعی شبکه.

DefaultTasksRepository بین لایه شبکه و لایه پایگاه داده مختصات یا واسطه می شود و همان چیزی است که داده ها را به لایه UI برمی گرداند.

لایه رابط کاربری (.addedittask، .statistics،.taskdetail، .tasks)

هر یک از بسته های لایه UI شامل یک قطعه و یک مدل view به همراه هر کلاس دیگری است که برای UI مورد نیاز است (مانند یک آداپتور برای لیست وظایف). TaskActivity اکتیویتی است که شامل تمام قطعات است.

ناوبری

ناوبری برای برنامه توسط مؤلفه ناوبری کنترل می شود. در فایل nav_graph.xml تعریف شده است. ناوبری در مدل های view با استفاده از کلاس Event فعال می شود. مدل‌های view نیز تعیین می‌کنند که چه آرگومان‌هایی باید منتقل شوند. قطعات Event را مشاهده می‌کنند و پیمایش واقعی بین صفحه‌ها را انجام می‌دهند.

در این کد لبه، نحوه تست مخازن، مشاهده مدل‌ها و قطعات با استفاده از دو برابر و تزریق وابستگی را یاد خواهید گرفت. قبل از اینکه در مورد این تست ها غوطه ور شوید، مهم است که استدلالی را که راهنمایی می کند این تست ها را چگونه و چگونه بنویسید، درک کنید.

این بخش برخی از بهترین روش‌های آزمایش را به طور کلی پوشش می‌دهد، زیرا برای Android اعمال می‌شود.

هرم تست

وقتی در مورد استراتژی تست فکر می کنیم، سه جنبه تست مرتبط وجود دارد:

  • محدوده — تست چه مقدار از کد را لمس می کند؟ آزمایش‌ها می‌توانند بر روی یک روش واحد، در کل برنامه یا جایی در میان اجرا شوند.
  • سرعت — تست با چه سرعتی اجرا می شود؟ سرعت تست می تواند از میلی ثانیه تا چند دقیقه متفاوت باشد.
  • وفاداری — آزمون چقدر «دنیای واقعی» است؟ به عنوان مثال، اگر بخشی از کدی که در حال آزمایش آن هستید نیاز به درخواست شبکه داشته باشد، آیا کد آزمایشی واقعاً این درخواست شبکه را انجام می دهد یا نتیجه را جعلی می کند؟ اگر آزمون واقعاً با شبکه صحبت می کند، به این معنی است که وفاداری بالاتری دارد. معاوضه این است که اجرای آزمایش ممکن است بیشتر طول بکشد، در صورت قطع شدن شبکه ممکن است منجر به خطا شود یا استفاده از آن پرهزینه باشد.

بین این جنبه ها مبادلات ذاتی وجود دارد. به عنوان مثال، سرعت و وفاداری یک مبادله هستند - هر چه تست سریعتر باشد، به طور کلی، وفاداری کمتر است، و بالعکس. یکی از روش های رایج برای تقسیم تست های خودکار به این سه دسته است:

  • تست های واحد — این تست ها بسیار متمرکز هستند که روی یک کلاس اجرا می شوند، معمولاً یک متد در آن کلاس. اگر تست واحد ناموفق باشد، می‌توانید دقیقاً بدانید که مشکل در کجای کدتان است. آنها وفاداری پایینی دارند زیرا در دنیای واقعی، برنامه شما بسیار بیشتر از اجرای یک متد یا کلاس است. آنها به اندازه ای سریع هستند که هر بار که کد خود را تغییر می دهید اجرا شوند. آنها اغلب به صورت محلی (در مجموعه منبع test ) اجرا می شوند. مثال: تست تک روش ها در مدل های view و مخازن.
  • تست‌های یکپارچه‌سازی : این تست‌ها تعامل چندین کلاس را آزمایش می‌کنند تا مطمئن شوند که هنگام استفاده با هم مطابق انتظار رفتار می‌کنند. یکی از راه‌های ساختاربندی تست‌های یکپارچه‌سازی این است که آن‌ها یک ویژگی واحد را آزمایش کنند، مانند توانایی ذخیره یک کار. آنها محدوده وسیع تری از کد را نسبت به تست های واحد آزمایش می کنند، اما همچنان برای اجرای سریع، در مقابل داشتن وفاداری کامل، بهینه شده اند. بسته به شرایط می‌توان آن‌ها را به صورت محلی یا به‌عنوان تست ابزار دقیق اجرا کرد. مثال: آزمایش تمام عملکردهای یک جفت مدل قطعه و نمایش.
  • تست های پایان به انتها (E2e) - ترکیبی از ویژگی ها را آزمایش کنید که با هم کار می کنند. آنها بخش‌های بزرگی از برنامه را آزمایش می‌کنند، استفاده واقعی را از نزدیک شبیه‌سازی می‌کنند و بنابراین معمولاً کند هستند. آنها بالاترین وفاداری را دارند و به شما می گویند که برنامه شما در واقع به طور کلی کار می کند. به طور کلی، این تست ها تست های ابزاری خواهند بود (در مجموعه منبع androidTest )
    مثال: راه اندازی کل برنامه و آزمایش چند ویژگی با هم.

نسبت پیشنهادی این تست‌ها اغلب توسط یک هرم نشان داده می‌شود که اکثریت قریب به اتفاق تست‌ها تست‌های واحد هستند.

معماری و آزمایش

توانایی شما برای آزمایش برنامه خود در تمام سطوح مختلف هرم آزمایشی به طور ذاتی با معماری برنامه شما مرتبط است. به عنوان مثال، یک برنامه کاربردی با معماری بسیار ضعیف ممکن است تمام منطق خود را در یک متد قرار دهد. ممکن است بتوانید یک تست پایان به انتها برای این کار بنویسید، زیرا این تست‌ها تمایل دارند بخش‌های بزرگی از برنامه را آزمایش کنند، اما در مورد تست‌های واحد نوشتن یا یکپارچه‌سازی چطور؟ با وجود همه کدها در یک مکان، آزمایش فقط کد مربوط به یک واحد یا ویژگی دشوار است.

یک رویکرد بهتر این است که منطق برنامه را به روش‌ها و کلاس‌های متعدد تقسیم کنیم و به هر قطعه اجازه می‌دهیم به صورت مجزا آزمایش شود. معماری راهی برای تقسیم و سازماندهی کد شما است که امکان تست واحد و یکپارچه سازی آسان تر را فراهم می کند. برنامه TO-DO که آزمایش می کنید از معماری خاصی پیروی می کند:



در این درس، نحوه تست بخش هایی از معماری فوق را به صورت مجزا مشاهده خواهید کرد:

  1. ابتدا مخزن را واحد تست خواهید کرد.
  2. سپس از یک تست دوبل در مدل view استفاده خواهید کرد که برای تست واحد و تست یکپارچه سازی مدل view ضروری است.
  3. در مرحله بعد، نوشتن تست‌های یکپارچه‌سازی برای قطعات و مدل‌های نمای آن‌ها را یاد خواهید گرفت.
  4. در نهایت، نوشتن تست‌های یکپارچه‌سازی را یاد خواهید گرفت که شامل مولفه Navigation باشد.

تست پایان تا پایان در درس بعدی پوشش داده خواهد شد.

وقتی برای بخشی از یک کلاس (یک متد یا مجموعه کوچکی از متدها) یک تست واحد می نویسید، هدف شما این است که فقط کد موجود در آن کلاس را آزمایش کنید .

آزمایش فقط کد در یک کلاس یا کلاس های خاص می تواند مشکل باشد. بیایید به یک مثال نگاه کنیم. کلاس data.source.DefaultTaskRepository را در مجموعه منبع main باز کنید. این مخزن برنامه است و کلاسی است که در مرحله بعدی تست های واحد را می نویسید.

هدف شما این است که فقط کدهای موجود در آن کلاس را آزمایش کنید. با این حال، DefaultTaskRepository برای عملکرد به کلاس‌های دیگر، مانند LocalTaskDataSource و RemoteTaskDataSource بستگی دارد. راه دیگری برای بیان این موضوع این است که LocalTaskDataSource و RemoteTaskDataSource وابستگی های DefaultTaskRepository هستند.

بنابراین هر متد در DefaultTaskRepository متدهایی را بر روی کلاس های منبع داده فراخوانی می کند، که به نوبه خود متدهای کلاس های دیگر را برای ذخیره اطلاعات در پایگاه داده یا برقراری ارتباط با شبکه فراخوانی می کند.



برای مثال، نگاهی به این روش در DefaultTasksRepo بیندازید.

    suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
        if (forceUpdate) {
            try {
                updateTasksFromRemoteDataSource()
            } catch (ex: Exception) {
                return Result.Error(ex)
            }
        }
        return tasksLocalDataSource.getTasks()
    }

getTasks یکی از "پایه" ترین تماس هایی است که ممکن است با مخزن خود برقرار کنید. این روش شامل خواندن از پایگاه داده SQLite و برقراری تماس های شبکه (تماس برای updateTasksFromRemoteDataSource ) است. این شامل کد بسیار بیشتری از کد مخزن است.

در اینجا چند دلیل خاص وجود دارد که چرا آزمایش مخزن سخت است:

  • شما باید به فکر ایجاد و مدیریت یک پایگاه داده باشید تا حتی ساده ترین تست ها را برای این مخزن انجام دهید. این سؤالاتی مانند "آیا این یک آزمون محلی یا ابزاری است؟" و اگر باید از AndroidX Test برای دریافت یک محیط اندروید شبیه سازی شده استفاده کنید.
  • اجرای برخی از بخش‌های کد، مانند کدهای شبکه، ممکن است زمان زیادی طول بکشد، یا حتی گاهی اوقات با شکست مواجه می‌شوند و آزمایش‌های طولانی‌مدت و پوسته‌پوستی ایجاد می‌کنند.
  • تست‌های شما ممکن است توانایی خود را برای تشخیص اینکه کدام کد مقصر خطای تست است، از دست بدهند. آزمایش‌های شما می‌توانند شروع به آزمایش کد غیر مخزن کنند، بنابراین، برای مثال، آزمایش‌های واحد «مخزن» فرضی شما ممکن است به دلیل مشکل در برخی از کدهای وابسته، مانند کد پایگاه داده، با شکست مواجه شوند.

تست دوبل

راه حل این است که وقتی در حال آزمایش مخزن هستید، از کد شبکه یا پایگاه داده واقعی استفاده نکنید ، بلکه از یک تست دوبل استفاده کنید. تست دو نسخه ای از یک کلاس است که به طور خاص برای آزمایش ساخته شده است. این به معنای جایگزینی نسخه واقعی یک کلاس در تست ها است. شبیه این است که یک بدلکار بازیگری است که در بدلکاری تخصص دارد و بازیگر واقعی را برای اقدامات خطرناک جایگزین می کند.

در اینجا چند نوع تست دوبل آورده شده است:

جعلی

یک تست دوتایی که یک پیاده‌سازی «کار» کلاس دارد، اما به گونه‌ای پیاده‌سازی شده است که برای آزمایش‌ها خوب است اما برای تولید نامناسب است.

مسخره کردن

یک تست دوبل که ردیابی می کند کدام یک از متدهای آن فراخوانی شده است. سپس بسته به اینکه متدهای آن به درستی فراخوانی شده باشند، در یک آزمون موفق می شود یا ناموفق می شود.

خرد

یک تست دوبل که شامل هیچ منطقی نیست و فقط آنچه را که برنامه ریزی کرده اید برمی گرداند. یک StubTaskRepository می توان طوری برنامه ریزی کرد که ترکیب خاصی از وظایف را از getTasks بازگرداند.

ساختگی

یک تست دوتایی که در اطراف ارسال می شود اما استفاده نمی شود، مثلاً اگر فقط باید آن را به عنوان یک پارامتر ارائه کنید. اگر NoOpTaskRepository داشتید، فقط TaskRepository بدون کد در هیچ یک از متدها پیاده سازی می کرد.

جاسوس

یک تست دوبل که همچنین برخی از اطلاعات اضافی را ردیابی می کند. برای مثال، اگر یک SpyTaskRepository ساخته اید، ممکن است تعداد دفعاتی که متد addTask فراخوانی شده است را پیگیری کند.

برای کسب اطلاعات بیشتر در مورد تست های دوتایی، تست در توالت: دوبل های آزمایشی خود را بشناسید .

رایج ترین تست های دوگانه مورد استفاده در اندروید Fakes و Mocks هستند.

در این کار، شما قصد دارید یک FakeDataSource تست دو به واحد، DefaultTasksRepository جدا از منابع داده واقعی ایجاد کنید.

مرحله 1: کلاس FakeDataSource را ایجاد کنید

در این مرحله شما می خواهید کلاسی به نام FakeDataSouce ایجاد کنید که دو تست LocalDataSource و RemoteDataSource خواهد بود.

  1. در مجموعه منبع آزمایشی ، روی New -> Package کلیک راست کنید.

  1. یک بسته داده با یک بسته منبع در داخل بسازید.
  2. یک کلاس جدید به نام FakeDataSource در بسته data/source ایجاد کنید.

مرحله 2: رابط TasksDataSource را پیاده سازی کنید

برای اینکه بتوانید از کلاس جدید FakeDataSource خود به عنوان دو تست استفاده کنید، باید بتواند جایگزین سایر منابع داده شود. این منابع داده عبارتند از TasksLocalDataSource و TasksRemoteDataSource .

  1. توجه کنید که هر دوی اینها چگونه رابط TasksDataSource را پیاده سازی می کنند.
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. کاری کنید که FakeDataSource TasksDataSource را پیاده سازی کند:
class FakeDataSource : TasksDataSource {

}

Android Studio شکایت خواهد کرد که روش‌های لازم را برای TasksDataSource پیاده‌سازی نکرده‌اید.

  1. از منوی رفع سریع استفاده کنید و Implement Members را انتخاب کنید.


  1. همه روش ها را انتخاب کرده و OK را فشار دهید.

مرحله 3: متد getTasks را در FakeDataSource پیاده سازی کنید

FakeDataSource نوع خاصی از تست دوگانه است که به آن جعلی گفته می شود. جعلی یک آزمایش دوگانه است که دارای یک پیاده‌سازی «در حال کار» از کلاس است، اما به گونه‌ای پیاده‌سازی شده است که برای آزمایش خوب است اما برای تولید نامناسب است. پیاده سازی "کار" به این معنی است که کلاس خروجی های واقعی را با ورودی های داده شده تولید می کند.

برای مثال، منبع داده جعلی شما به شبکه متصل نمی‌شود یا چیزی را در پایگاه داده ذخیره نمی‌کند، بلکه فقط از یک لیست درون حافظه استفاده می‌کند. این "همانطور که انتظار دارید کار می کند" زیرا روش های دریافت یا ذخیره کارها نتایج مورد انتظار را به دست خواهند آورد، اما هرگز نمی توانید از این پیاده سازی در تولید استفاده کنید، زیرا در سرور یا پایگاه داده ذخیره نمی شود.

یک FakeDataSource

  • به شما امکان می دهد کد را در DefaultTasksRepository بدون نیاز به تکیه بر پایگاه داده یا شبکه واقعی آزمایش کنید.
  • یک پیاده سازی "به اندازه کافی واقعی" برای آزمایش ها ارائه می دهد.
  1. سازنده FakeDataSource را برای ایجاد یک var به نام tasks تغییر دهید که MutableList<Task>? با مقدار پیش فرض یک لیست خالی قابل تغییر.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


این لیستی از وظایفی است که به عنوان یک پایگاه داده یا پاسخ سرور "جعل" می شوند. در حال حاضر، هدف آزمایش روش getTasks مخزن است. این روش‌های getTasks ، deleteAllTasks و saveTask منبع داده را فراخوانی می‌کند.

یک نسخه جعلی از این روش ها بنویسید:

  1. getTasks بنویسید: اگر tasks null نیستند، یک نتیجه Success را برگردانید. اگر tasks null است، یک نتیجه Error را برگردانید.
  2. نوشتن deleteAllTasks : لیست وظایف قابل تغییر را پاک کنید.
  3. saveTask بنویسید: وظیفه را به لیست اضافه کنید.

این روش‌ها که برای FakeDataSource پیاده‌سازی شده‌اند، شبیه کد زیر هستند.

override suspend fun getTasks(): Result<List<Task>> {
    tasks?.let { return Success(ArrayList(it)) }
    return Error(
        Exception("Tasks not found")
    )
}


override suspend fun deleteAllTasks() {
    tasks?.clear()
}

override suspend fun saveTask(task: Task) {
    tasks?.add(task)
}

در صورت نیاز، بیانیه‌های واردات آمده است:

import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task

این مشابه نحوه عملکرد منابع داده محلی و راه دور واقعی است.

در این مرحله، می‌خواهید از تکنیکی به نام تزریق وابستگی دستی استفاده کنید تا بتوانید از تست جعلی که ایجاد کرده‌اید استفاده کنید.

مسئله اصلی این است که شما یک FakeDataSource دارید، اما نحوه استفاده از آن در تست ها مشخص نیست. باید جایگزین TasksRemoteDataSource و TasksLocalDataSource شود، اما فقط در تست‌ها. هر دو TasksRemoteDataSource و TasksLocalDataSource وابستگی های DefaultTasksRepository هستند، به این معنی که DefaultTasksRepositories برای اجرا به این کلاس ها نیاز دارد یا به آنها "وابسته" دارد.

در حال حاضر، وابستگی ها در متد init DefaultTasksRepository ساخته می شوند.

DefaultTasksRepository.kt

class DefaultTasksRepository private constructor(application: Application) {

    private val tasksRemoteDataSource: TasksDataSource
    private val tasksLocalDataSource: TasksDataSource

   // Some other code

    init {
        val database = Room.databaseBuilder(application.applicationContext,
            ToDoDatabase::class.java, "Tasks.db")
            .build()

        tasksRemoteDataSource = TasksRemoteDataSource
        tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
    }
    // Rest of class
}

از آنجایی که شما در حال ایجاد و تخصیص taskLocalDataSource و tasksRemoteDataSource در DefaultTasksRepository هستید، اساساً کدگذاری سختی دارند. هیچ راهی برای تعویض در تست دوبل شما وجود ندارد.

کاری که می‌خواهید انجام دهید، این است که این منابع داده را به‌جای کدگذاری سخت، در اختیار کلاس قرار دهید . ارائه وابستگی ها به عنوان تزریق وابستگی شناخته می شود. روش های مختلفی برای ارائه وابستگی ها و در نتیجه انواع مختلفی از تزریق وابستگی وجود دارد.

Constructor Dependency Injection به شما این امکان را می دهد که با ارسال آن به سازنده، دو برابر تست را تعویض کنید.

بدون تزریق

تزریق

مرحله 1: از Injection وابستگی سازنده در DefaultTasksRepository استفاده کنید

  1. سازنده DefaultTaskRepository را از دریافت یک Application به دریافت هر دو منبع داده و توزیع کننده Coroutine تغییر دهید (که شما همچنین باید آن را برای تست های خود تعویض کنید - این با جزئیات بیشتر در بخش درس سوم در مورد کوروتین ها توضیح داده شده است).

DefaultTasksRepository.kt

// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }

// WITH

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
  1. چون وابستگی‌ها را وارد کردید، متد init حذف کنید. دیگر نیازی به ایجاد وابستگی ندارید.
  2. همچنین متغیرهای نمونه قدیمی را حذف کنید. شما آنها را در سازنده تعریف می کنید:

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. در نهایت، متد getRepository را برای استفاده از سازنده جدید به روز کنید:

DefaultTasksRepository.kt

    companion object {
        @Volatile
        private var INSTANCE: DefaultTasksRepository? = null

        fun getRepository(app: Application): DefaultTasksRepository {
            return INSTANCE ?: synchronized(this) {
                val database = Room.databaseBuilder(app,
                    ToDoDatabase::class.java, "Tasks.db")
                    .build()
                DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                    INSTANCE = it
                }
            }
        }
    }

شما اکنون از تزریق وابستگی سازنده استفاده می کنید!

مرحله 2: از FakeDataSource خود در آزمایشات خود استفاده کنید

اکنون که کد شما از تزریق وابستگی سازنده استفاده می کند، می توانید از منبع داده جعلی خود برای آزمایش DefaultTasksRepository خود استفاده کنید.

  1. روی نام کلاس DefaultTasksRepository کلیک راست کرده و Generate و سپس Test را انتخاب کنید.
  2. دستورات را برای ایجاد DefaultTasksRepositoryTest در مجموعه منبع آزمایشی دنبال کنید.
  3. در بالای کلاس DefaultTasksRepositoryTest جدید، متغیرهای عضو را در زیر اضافه کنید تا داده‌ها را در منابع داده جعلی خود نشان دهید.

DefaultTasksRepositoryTest.kt

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }
  1. سه متغیر، دو متغیر عضو FakeDataSource (یکی برای هر منبع داده برای مخزن شما) و یک متغیر برای DefaultTasksRepository که آن را آزمایش خواهید کرد، ایجاد کنید.

DefaultTasksRepositoryTest.kt

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

روشی برای راه اندازی و مقداردهی اولیه یک DefaultTasksRepository قابل آزمایش بسازید. این DefaultTasksRepository از دو تست شما، FakeDataSource استفاده خواهد کرد.

  1. روشی به نام createRepository ایجاد کنید و آن را با @Before حاشیه نویسی کنید.
  2. منابع داده جعلی خود را با استفاده از لیست های remoteTasks و localTasks نمونه سازی کنید.
  3. tasksRepository خود را با استفاده از دو منبع داده جعلی که ایجاد کرده‌اید و Dispatchers.Unconfined نمونه‌سازی کنید.

روش نهایی باید مانند کد زیر باشد.

DefaultTasksRepositoryTest.kt

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

مرحله 3: DefaultTasksRepository getTasks() Test را بنویسید

زمان نوشتن یک تست DefaultTasksRepository است!

  1. یک تست برای متد getTasks مخزن بنویسید. بررسی کنید که وقتی getTasks با true فرا می‌خوانید (به این معنی که باید از منبع داده راه دور مجدداً بارگیری شود)، داده‌ها را از منبع داده راه دور بازگرداند (بر خلاف منبع داده محلی).

DefaultTasksRepositoryTest.kt

@Test
    fun getTasks_requestsAllTasksFromRemoteDataSource(){
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

هنگام فراخوانی getTasks:

مرحله 4: runBlockingTest را اضافه کنید

خطای Coroutine انتظار می رود زیرا getTasks یک تابع suspend است و برای فراخوانی آن باید یک Coroutine راه اندازی کنید. برای آن، شما به یک محدوده کاری نیاز دارید. برای رفع این خطا، باید چند وابستگی gradle را برای مدیریت راه‌اندازی کوروتین‌ها در تست‌های خود اضافه کنید.

  1. با استفاده از testImplementation وابستگی های مورد نیاز برای تست کوروتین ها را به مجموعه منبع تست اضافه کنید.

app/build.gradle

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

همگام سازی را فراموش نکنید!

kotlinx-coroutines-test کتابخانه تست coroutines است که به طور خاص برای آزمایش کوروتین ها در نظر گرفته شده است. برای اجرای تست های خود، از تابع runBlockingTest استفاده کنید. این تابعی است که توسط کتابخانه تست coroutines ارائه شده است. یک بلوک از کد را می گیرد و سپس این بلوک کد را در یک زمینه ویژه کاری اجرا می کند که به صورت همزمان و بلافاصله اجرا می شود، به این معنی که اقدامات به ترتیب قطعی انجام می شوند. این اساسا باعث می‌شود که کوروتین‌های شما مانند برنامه‌های غیرکوروتین اجرا شوند، بنابراین برای آزمایش کد در نظر گرفته شده است.

هنگام فراخوانی یک تابع suspend از runBlockingTest در کلاس های آزمایشی خود استفاده کنید. شما در مورد نحوه عملکرد runBlockingTest و نحوه آزمایش کوروتین ها در کدهای بعدی این مجموعه اطلاعات بیشتری کسب خواهید کرد.

  1. @ExperimentalCoroutinesApi را بالای کلاس اضافه کنید. این نشان می‌دهد که می‌دانید از یک api کوروتین آزمایشی ( runBlockingTest ) در کلاس استفاده می‌کنید. بدون آن، شما یک هشدار دریافت خواهید کرد.
  2. در DefaultTasksRepositoryTest خود، runBlockingTest اضافه کنید تا در کل آزمایش شما به عنوان یک "بلاک" از کد استفاده شود.

این تست نهایی شبیه کد زیر است.

DefaultTasksRepositoryTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test


@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

    @Test
    fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

}
  1. تست getTasks_requestsAllTasksFromRemoteDataSource جدید خود را اجرا کنید و تأیید کنید که کار می کند و خطا برطرف شده است!

شما به تازگی نحوه تست واحد یک مخزن را دیدید. در این مراحل بعدی، مجدداً از تزریق وابستگی استفاده می‌کنید و دوبار تست دیگری ایجاد می‌کنید – این بار برای نشان دادن نحوه نوشتن تست‌های واحد و ادغام برای مدل‌های view خود.

تست‌های واحد فقط باید کلاس یا روشی را که به آن علاقه‌مندید آزمایش کنند. این به عنوان تست در انزوا شناخته می‌شود، که در آن شما به وضوح "واحد" خود را جدا می‌کنید و فقط کدی را که بخشی از آن واحد است آزمایش می‌کنید.

بنابراین TasksViewModelTest فقط باید کد TasksViewModel را آزمایش کند - نباید در پایگاه داده، شبکه یا کلاس های مخزن تست شود. بنابراین برای مدل‌های view خود، دقیقاً مانند آنچه که برای مخزن خود انجام دادید، یک مخزن جعلی ایجاد می‌کنید و برای استفاده از آن در آزمایش‌های خود از تزریق وابستگی استفاده می‌کنید.

در این کار، تزریق وابستگی را برای مشاهده مدل ها اعمال می کنید.

مرحله 1. یک رابط TasksRepository ایجاد کنید

اولین قدم برای استفاده از تزریق وابستگی سازنده، ایجاد یک رابط مشترک بین کلاس جعلی و واقعی است.

این در عمل چگونه به نظر می رسد؟ به TasksRemoteDataSource ، TasksLocalDataSource و FakeDataSource نگاه کنید، و توجه کنید که همه آنها یک رابط مشترک دارند: TasksDataSource . این به شما امکان می دهد در سازنده DefaultTasksRepository بگویید که یک TasksDataSource را وارد می کنید.

DefaultTasksRepository.kt

class DefaultTasksRepository(
   private val tasksRemoteDataSource: TasksDataSource,
   private val tasksLocalDataSource: TasksDataSource,
   private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {

این همان چیزی است که به ما امکان می دهد در FakeDataSource شما مبادله کنیم!

در مرحله بعد، همانند منابع داده، یک رابط برای DefaultTasksRepository ایجاد کنید. باید شامل تمام متدهای عمومی (سطح API عمومی) DefaultTasksRepository باشد.

  1. DefaultTasksRepository را باز کرده و روی نام کلاس کلیک راست کنید . سپس Refactor -> Extract -> Interface را انتخاب کنید.

  1. برای جداسازی فایل Extract را انتخاب کنید.

  1. در پنجره Extract Interface ، نام رابط را به TasksRepository تغییر دهید.
  2. در قسمت Members to form interface ، همه اعضا به جز دو عضو همراه و متدهای خصوصی را بررسی کنید.


  1. روی Refactor کلیک کنید. رابط TasksRepository جدید باید در بسته داده/منبع ظاهر شود.

و DefaultTasksRepository اکنون TasksRepository را پیاده سازی می کند.

  1. برنامه خود را اجرا کنید (نه تست ها) تا مطمئن شوید که همه چیز هنوز در حالت کار است.

مرحله 2. FakeTestRepository را ایجاد کنید

اکنون که اینترفیس را دارید، می توانید تست دوبل DefaultTaskRepository ایجاد کنید.

  1. در مجموعه منبع آزمایشی ، در data/source فایل Kotlin و کلاس FakeTestRepository.kt را ایجاد کنید و از رابط TasksRepository گسترش دهید.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

به شما گفته می شود که باید متدهای رابط را پیاده سازی کنید.

  1. ماوس را روی خطا نگه دارید تا منوی پیشنهاد را مشاهده کنید، سپس کلیک کرده و Implement Members را انتخاب کنید.
  1. همه روش ها را انتخاب کرده و OK را فشار دهید.

مرحله 3. روش های FakeTestRepository را پیاده سازی کنید

اکنون یک کلاس FakeTestRepository با متدهای "نیست پیاده سازی" دارید. مشابه نحوه پیاده‌سازی FakeDataSource ، FakeTestRepository به جای پرداختن به میانجی‌گری پیچیده بین منابع داده محلی و راه دور، توسط یک ساختار داده پشتیبانی می‌شود.

توجه داشته باشید که FakeTestRepository شما نیازی به استفاده از FakeDataSource یا هر چیز دیگری ندارد. فقط باید خروجی های جعلی واقع بینانه را برگرداند. شما از LinkedHashMap برای ذخیره لیست وظایف و MutableLiveData برای وظایف قابل مشاهده خود استفاده خواهید کرد.

  1. در FakeTestRepository ، هم یک متغیر LinkedHashMap که لیست فعلی وظایف را نشان می دهد و هم یک MutableLiveData برای وظایف قابل مشاهده خود اضافه کنید.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()


    // Rest of class
}

روش های زیر را اجرا کنید:

  1. getTasks — این روش باید tasksServiceData را گرفته و با استفاده از tasksServiceData.values.toList() آن را به یک لیست تبدیل کند و سپس آن را به عنوان نتیجه Success برگرداند.
  2. refreshTasks - مقدار observableTasks به‌روزرسانی می‌کند تا همان چیزی باشد که توسط getTasks() برگردانده می‌شود.
  3. observeTasks — با استفاده از runBlocking یک برنامه مشترک ایجاد می کند و refreshTasks اجرا می کند، سپس observableTasks برمی گرداند.

در زیر کد آن متدها آمده است.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        return Result.Success(tasksServiceData.values.toList())
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    // Rest of class

}

مرحله 4. روشی برای آزمایش به addTasks اضافه کنید

هنگام آزمایش، بهتر است برخی از Tasks از قبل در مخزن خود داشته باشید. می‌توانید چندین بار با saveTask تماس بگیرید، اما برای آسان‌تر کردن این کار، یک روش کمکی مخصوص آزمایش‌ها اضافه کنید که به شما امکان می‌دهد وظایف را اضافه کنید.

  1. متد addTasks را اضافه کنید که چندین کار vararg را انجام می‌دهد، هر کدام را به HashMap اضافه می‌کند و سپس کارها را تازه می‌کند.

FakeTestRepository.kt

    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }

در این مرحله شما یک مخزن جعلی برای آزمایش با چند روش کلیدی پیاده سازی شده دارید. بعد، از این در تست های خود استفاده کنید!

در این کار از یک کلاس جعلی در داخل ViewModel استفاده می کنید. از تزریق وابستگی سازنده برای دریافت دو منبع داده از طریق تزریق وابستگی سازنده با افزودن یک متغیر TasksRepository به سازنده TasksViewModel استفاده کنید.

این فرآیند با مدل‌های view کمی متفاوت است زیرا شما آنها را مستقیماً نمی‌سازید. به عنوان مثال:

class TasksFragment : Fragment() {

    private val viewModel by viewModels<TasksViewModel>()
    
    // Rest of class...

}


همانطور که در کد بالا وجود دارد، شما از ویژگی delegate viewModel's استفاده می کنید که مدل view را ایجاد می کند. برای تغییر نحوه ساخت مدل view، باید ViewModelProvider.Factory را اضافه کرده و از آن استفاده کنید. اگر با ViewModelProvider.Factory آشنا نیستید، می‌توانید در اینجا درباره آن اطلاعات بیشتری کسب کنید.

مرحله 1. یک ViewModelFactory در TasksViewModel بسازید و از آن استفاده کنید

شما با به روز رسانی کلاس ها و تست های مربوط به صفحه Tasks شروع می کنید.

  1. TasksViewModel باز کنید .
  2. سازنده TasksViewModel را تغییر دهید تا به جای ساختن آن در داخل کلاس، TasksRepository را بگیرد.

TasksViewModel.kt

// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() { 
    // Rest of class 
}

از آنجایی که سازنده را تغییر دادید، اکنون باید از یک کارخانه برای ساخت TasksViewModel استفاده کنید. کلاس کارخانه را در همان فایل TasksViewModel قرار دهید، اما می توانید آن را در فایل خودش نیز قرار دهید.

  1. در پایین فایل TasksViewModel ، خارج از کلاس، یک TasksViewModelFactory اضافه کنید که یک TasksRepository ساده را می گیرد.

TasksViewModel.kt

@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TasksViewModel(tasksRepository) as T)
}


این روش استانداردی است که شما نحوه ساخت ViewModel ها را تغییر می دهید. اکنون که کارخانه را دارید، از آن در هر کجا که مدل view خود را می سازید استفاده کنید.

  1. برای استفاده از کارخانه، TasksFragment به روز کنید.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TasksViewModel>()

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. کد برنامه خود را اجرا کنید و مطمئن شوید که همه چیز همچنان کار می کند!

مرحله 2. از FakeTestRepository در TasksViewModelTest استفاده کنید

اکنون به جای استفاده از مخزن واقعی در تست های مدل view خود، می توانید از مخزن جعلی استفاده کنید.

  1. TasksViewModelTest را باز کنید .
  2. یک ویژگی FakeTestRepository در TasksViewModelTest اضافه کنید.

TaskViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeTestRepository
    
    // Rest of class
}
  1. روش setupViewModel به روز کنید تا یک FakeTestRepository با سه کار بسازید و سپس tasksViewModel با این مخزن بسازید.

TasksViewModelTest.kt

    @Before
    fun setupViewModel() {
        // We initialise the tasks to 3, with one active and two completed
        tasksRepository = FakeTestRepository()
        val task1 = Task("Title1", "Description1")
        val task2 = Task("Title2", "Description2", true)
        val task3 = Task("Title3", "Description3", true)
        tasksRepository.addTasks(task1, task2, task3)

        tasksViewModel = TasksViewModel(tasksRepository)
        
    }
  1. از آنجایی که دیگر از کد AndroidX Test ApplicationProvider.getApplicationContext استفاده نمی کنید، می توانید حاشیه نویسی @RunWith(AndroidJUnit4::class) را نیز حذف کنید.
  2. تست های خود را اجرا کنید، مطمئن شوید که همه آنها هنوز کار می کنند!

با استفاده از تزریق وابستگی سازنده، اکنون DefaultTasksRepository به عنوان یک وابستگی حذف کرده‌اید و آن را با FakeTestRepository خود در تست‌ها جایگزین کرده‌اید.

مرحله 3. همچنین TaskDetail Fragment و ViewModel را به روز کنید

دقیقاً همان تغییرات را برای TaskDetailFragment و TaskDetailViewModel ایجاد کنید. این کد را برای زمانی که تست های بعدی TaskDetail را می نویسید آماده می کند.

  1. TaskDetailViewModel باز کنید .
  2. سازنده را به روز کنید:

TaskDetailViewModel.kt

// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TaskDetailViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }
  1. در پایین فایل TaskDetailViewModel ، خارج از کلاس، یک TaskDetailViewModelFactory اضافه کنید.

TaskDetailViewModel.kt

@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TaskDetailViewModel(tasksRepository) as T)
}
  1. برای استفاده از کارخانه، TasksFragment به روز کنید.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. کد خود را اجرا کنید و مطمئن شوید که همه چیز کار می کند.

اکنون می توانید به جای مخزن واقعی در TasksFragment و TasksDetailFragment FakeTestRepository استفاده کنید.

در مرحله بعد تست های ادغام را برای آزمایش تعامل قطعه و نمایش مدل خود می نویسید. خواهید فهمید که آیا کد مدل مشاهده شما به طور مناسب UI شما را به روز می کند یا خیر. برای این کار شما استفاده می کنید

  • الگوی سرویس دهنده
  • کتابخانه های اسپرسو و مسخره

تست های ادغام تعامل چندین کلاس را آزمایش کنید تا اطمینان حاصل شود که آنها همانطور که انتظار می رفت هنگام استفاده با هم رفتار می کنند. این آزمایشات را می توان به صورت محلی (مجموعه منبع test ) یا به عنوان تست های ابزار دقیق (مجموعه منبع androidTest ) اجرا کرد.

در مورد شما ، برای آزمایش ویژگی های اصلی این قطعه ، هر قطعه و تست های ادغام نوشتن را برای قطعه و مدل مشاهده می کنید.

مرحله 1. وابستگی های Gradle را اضافه کنید

  1. وابستگی های درجه زیر را اضافه کنید.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "junit:junit:$junitVersion"
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

    // Testing code should not be included in the main code.
    // Once https://issuetracker.google.com/128612536 is fixed this can be fixed.

    implementation "androidx.fragment:fragment-testing:$fragmentVersion"
    implementation "androidx.test:core:$androidXTestCoreVersion"

این وابستگی ها عبارتند از:

  • junit:junit - Junit ، که برای نوشتن بیانیه های آزمون اساسی لازم است.
  • androidx.test:core
  • kotlinx-coroutines-test کتابخانه تست Coroutines
  • androidx.fragment:fragment-testing برای ایجاد قطعات در آزمایشات و تغییر وضعیت آنها.

از آنجا که شما از این کتابخانه ها در مجموعه منبع androidTest استفاده می کنید ، از androidTestImplementation استفاده کنید تا آنها را به عنوان وابستگی اضافه کنید.

مرحله 2. یک کلاس TaskDetailFragmentTest درست کنید

TaskDetailFragment اطلاعات مربوط به یک کار واحد را نشان می دهد.

شما با نوشتن یک آزمون قطعه برای TaskDetailFragment شروع می کنید زیرا عملکرد نسبتاً اساسی در مقایسه با سایر قطعات دارد.

  1. taskdetail.TaskDetailFragment باز کنید .
  2. همانطور که قبلاً انجام داده اید ، آزمایشی برای TaskDetailFragment ایجاد کنید . گزینه های پیش فرض را بپذیرید و آن را در مجموعه منبع AndroidTest قرار دهید (نه مجموعه منبع test ).

  1. حاشیه نویسی های زیر را به کلاس TaskDetailFragmentTest اضافه کنید.

TaskDetailFragmentTest.kt

@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

}

هدف از این حاشیه نویسی:

  • @MediumTest تست را به عنوان تست ادغام "زمان متوسط در زمان" (در مقابل تست های واحد @SmallTest و @LargeTest تست های بزرگ پایان به پایان) علامت گذاری می کند. این به شما کمک می کند تا گروهی را انتخاب کنید و اندازه آن را انتخاب کنید.
  • @RunWith(AndroidJUnit4::class) - در هر کلاس با استفاده از تست Androidx استفاده می شود.

مرحله 3. یک قطعه را از یک آزمایش راه اندازی کنید

در این کار ، شما می خواهید TaskDetailFragment با استفاده از کتابخانه تست Androidx راه اندازی کنید. FragmentScenario یک کلاس از AndroidX است که به دور یک قطعه می پیچد و به شما کنترل مستقیم چرخه عمر این قطعه را برای آزمایش می دهد. برای نوشتن تست برای قطعات ، شما برای قطعه ای که در حال آزمایش هستید ( TaskDetailFragment ) یک FragmentScenario ایجاد می کنید.

  1. این تست را در TaskDetailFragmentTest کپی کنید .

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

این کد در بالا:

  • یک کار ایجاد می کند.
  • یک Bundle ایجاد می کند ، که نشان دهنده آرگومان های قطعه برای کاری است که به این قطعه منتقل می شود).
  • عملکرد launchFragmentInContainer با این بسته نرم افزاری و یک موضوع ، یک FragmentScenario ایجاد می کند.

این هنوز یک تست تمام نشده است ، زیرا چیزی را ادعا نمی کند. در حال حاضر ، آزمایش را اجرا کنید و آنچه را که اتفاق می افتد مشاهده کنید.

  1. این یک تست ابزار دقیق است ، بنابراین اطمینان حاصل کنید که شبیه ساز یا دستگاه شما قابل مشاهده است.
  2. آزمون را اجرا کنید .

چند مورد باید اتفاق بیفتد.

  • اول ، از آنجا که این یک تست ابزار دقیق است ، آزمایش روی دستگاه فیزیکی شما (در صورت اتصال) یا یک شبیه ساز اجرا می شود.
  • باید این قطعه را راه اندازی کند.
  • توجه کنید که چگونه از طریق هر قطعه دیگر حرکت نمی کند یا منوهای مرتبط با فعالیت را دارد - این فقط قطعه است.

سرانجام ، از نزدیک نگاه کنید و توجه کنید که این قطعه "بدون داده" می گوید زیرا با موفقیت داده های کار را بارگیری نمی کند.

آزمایش شما هر دو نیاز به بارگیری TaskDetailFragment (که انجام داده اید) و ادعا می کنند که داده ها به درستی بارگیری شده اند. چرا هیچ داده ای وجود ندارد؟ این امر به این دلیل است که شما یک کار ایجاد کرده اید ، اما آن را در مخزن ذخیره نکردید.

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // This DOES NOT save the task anywhere
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

شما این FakeTestRepository دارید ، اما به راهی نیاز دارید تا مخزن واقعی خود را با جعلی خود جایگزین کنید. شما این کار را بعد انجام خواهید داد!

در این کار ، مخزن جعلی خود را با استفاده از یک ServiceLocator ارائه می دهید. این به شما امکان می دهد قطعه خود را بنویسید و تست های ادغام مدل را مشاهده کنید.

شما نمی توانید از تزریق وابستگی سازنده در اینجا استفاده کنید ، همانطور که قبلاً انجام دادید ، در صورت نیاز به ارائه وابستگی به مدل نمایش یا مخزن. تزریق وابستگی سازنده مستلزم ساخت کلاس است. قطعات و فعالیت ها نمونه ای از کلاس هایی است که شما نمی سازید و به طور کلی به سازنده دسترسی ندارید.

از آنجا که شما این قطعه را نمی سازید ، نمی توانید از تزریق وابستگی سازنده برای تعویض تست مخزن دو برابر ( FakeTestRepository ) به این قطعه استفاده کنید. در عوض ، از الگوی سرویس یاب استفاده کنید. الگوی سرویس یاب سرویس جایگزینی برای تزریق وابستگی است. این شامل ایجاد یک کلاس Singleton به نام "Service Locator" است که هدف آن فراهم کردن وابستگی ها ، چه برای کد منظم و چه برای آزمون است. در کد برنامه معمولی (مجموعه منبع main ) ، همه این وابستگی ها وابستگی های معمولی برنامه هستند. برای تست ها ، شما یاب سرویس را اصلاح می کنید تا نسخه های دوتایی وابستگی ها را ارائه دهید.

استفاده از یاب سرویس


با استفاده از یاب سرویس

برای این برنامه CodeLab ، موارد زیر را انجام دهید:

  1. یک کلاس یاب سرویس ایجاد کنید که قادر به ساخت و ذخیره مخزن باشد. به طور پیش فرض ، یک مخزن "عادی" ایجاد می کند.
  2. کد خود را به گونه ای تغییر دهید تا در صورت نیاز به مخزن ، از Service Locator استفاده کنید.
  3. در کلاس تست خود ، با یک روش در یاب سرویس تماس بگیرید که مخزن "عادی" را با دو برابر تست خود تعویض می کند.

مرحله 1. ServiceLocator را ایجاد کنید

بیایید یک کلاس ServiceLocator بسازیم. این در منبع اصلی تنظیم شده با بقیه کد برنامه زندگی می کند زیرا توسط کد برنامه اصلی استفاده می شود.

توجه: ServiceLocator یک تک آهنگ است ، بنابراین از کلمه کلیدی object Kotlin برای کلاس استفاده کنید.

  1. File servicelocator.kt را در سطح بالای مجموعه منبع اصلی ایجاد کنید.
  2. یک object به نام ServiceLocator را تعریف کنید.
  3. متغیرهای نمونه database و repository را ایجاد کنید و هر دو را null تنظیم کنید.
  4. مخزن را با @Volatile حاشیه نویسی کنید زیرا می تواند توسط چندین موضوع استفاده شود ( @Volatile در اینجا به تفصیل توضیح داده شده است).

کد شما باید به صورت زیر نشان داده شود.

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

}

در حال حاضر تنها کاری که ServiceLocator شما باید انجام دهد این است که بدانید چگونه یک TasksRepository برگردانید. این یک DefaultTasksRepository از قبل موجود را باز می گرداند یا در صورت لزوم یک DefaultTasksRepository جدید را ساخته و باز می گرداند.

توابع زیر را تعریف کنید:

  1. provideTasksRepository - یک مخزن موجود در حال حاضر موجود را فراهم می کند یا یک مورد جدید ایجاد می کند. این روش باید در this synchronized شود تا در موقعیت هایی با چندین موضوع اجرا شود ، تا کنون به طور تصادفی دو نمونه مخزن را ایجاد کند.
  2. createTasksRepository - کد برای ایجاد یک مخزن جدید. با createTaskLocalDataSource تماس می گیرید و یک TasksRemoteDataSource جدید ایجاد می کند.
  3. createTaskLocalDataSource - کد برای ایجاد یک منبع داده محلی جدید. createDataBase تماس می گیرد.
  4. createDataBase - کد برای ایجاد یک پایگاه داده جدید.

کد تکمیل شده در زیر است.

servicelocator.kt

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

    fun provideTasksRepository(context: Context): TasksRepository {
        synchronized(this) {
            return tasksRepository ?: createTasksRepository(context)
        }
    }

    private fun createTasksRepository(context: Context): TasksRepository {
        val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
        tasksRepository = newRepo
        return newRepo
    }

    private fun createTaskLocalDataSource(context: Context): TasksDataSource {
        val database = database ?: createDataBase(context)
        return TasksLocalDataSource(database.taskDao())
    }

    private fun createDataBase(context: Context): ToDoDatabase {
        val result = Room.databaseBuilder(
            context.applicationContext,
            ToDoDatabase::class.java, "Tasks.db"
        ).build()
        database = result
        return result
    }
}

مرحله 2 از Servicelocator در برنامه استفاده کنید

شما می خواهید در کد برنامه اصلی خود (نه تست های خود) تغییری ایجاد کنید تا مخزن را در یک مکان ، ServiceLocator خود ایجاد کنید.

این مهم است که شما فقط یک نمونه از کلاس مخزن را تهیه کنید. برای اطمینان از این امر ، در کلاس برنامه کاربردی من از سرویس دهنده خدمات استفاده خواهید کرد.

  1. در سطح بالای سلسله مراتب بسته بندی خود TodoApplication باز کنید و یک val خود را ایجاد کنید و مخزن آن را اختصاص دهید که با استفاده از ServiceLocator.provideTaskRepository به دست می آید.

todoapplication.kt

class TodoApplication : Application() {

    val taskRepository: TasksRepository
        get() = ServiceLocator.provideTasksRepository(this)

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
    }
}

اکنون که یک مخزن در برنامه ایجاد کرده اید ، می توانید روش قدیمی getRepository را در DefaultTasksRepository حذف کنید.

  1. DefaultTasksRepository را باز کرده و شیء همراه را حذف کنید.

defaulttasksrepository.kt

// DELETE THIS COMPANION OBJECT
companion object {
    @Volatile
    private var INSTANCE: DefaultTasksRepository? = null

    fun getRepository(app: Application): DefaultTasksRepository {
        return INSTANCE ?: synchronized(this) {
            val database = Room.databaseBuilder(app,
                ToDoDatabase::class.java, "Tasks.db")
                .build()
            DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                INSTANCE = it
            }
        }
    }
}

اکنون در هر کجا که از getRepository استفاده می کردید ، به جای آن از taskRepository برنامه استفاده کنید. این تضمین می کند که به جای اینکه مخزن را مستقیماً تهیه کنید ، هر نوع مخزن را که ServiceLocator ارائه شده است دریافت می کنید.

  1. TaskDetailFragement را باز کنید و فراخوانی برای getRepository را در بالای کلاس پیدا کنید.
  2. این تماس را با تماس تلفنی جایگزین کنید که مخزن را از TodoApplication دریافت می کند.

TaskDetailFragment.kt

// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}

// WITH this code

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
  1. همین کار را برای TasksFragment انجام دهید.

TaskSfragment.kt

// REPLACE this code
    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
    }


// WITH this code

    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
    }
  1. برای StatisticsViewModel و AddEditTaskViewModel ، کدی را که مخزن را به دست می آورد ، به روز کنید تا از مخزن از TodoApplication استفاده کنید.

TaskSfragment.kt

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. برنامه خود را اجرا کنید (نه آزمون)!

از آنجا که شما فقط مجدداً مورد استفاده قرار می گیرید ، برنامه باید بدون مشکل همین کار را اجرا کند.

مرحله 3. ایجاد fakeandidoidtesterpository

شما قبلاً در مجموعه منبع آزمایش یک FakeTestRepository دارید. شما نمی توانید کلاس های تست را بین مجموعه های test و androidTest به طور پیش فرض به اشتراک بگذارید. بنابراین ، شما باید یک کلاس FakeTestRepository کپی در مجموعه منبع androidTest بسازید و آن را FakeAndroidTestRepository بنامید.

  1. روی مجموعه منبع androidTest راست کلیک کرده و یک بسته داده را تهیه کنید. دوباره کلیک راست کرده و یک بسته منبع درست کنید.
  2. یک کلاس جدید را در این بسته منبع به نام FakeAndroidTestRepository.kt ایجاد کنید.
  3. کد زیر را در آن کلاس کپی کنید.

fakeandroidtestrepository.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap



class FakeAndroidTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private var shouldReturnError = false

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    fun setReturnError(value: Boolean) {
        shouldReturnError = value
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override suspend fun refreshTask(taskId: String) {
        refreshTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    override fun observeTask(taskId: String): LiveData<Result<Task>> {
        runBlocking { refreshTasks() }
        return observableTasks.map { tasks ->
            when (tasks) {
                is Result.Loading -> Result.Loading
                is Error -> Error(tasks.exception)
                is Success -> {
                    val task = tasks.data.firstOrNull() { it.id == taskId }
                        ?: return@map Error(Exception("Not found"))
                    Success(task)
                }
            }
        }
    }

    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        tasksServiceData[taskId]?.let {
            return Success(it)
        }
        return Error(Exception("Could not find task"))
    }

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        return Success(tasksServiceData.values.toList())
    }

    override suspend fun saveTask(task: Task) {
        tasksServiceData[task.id] = task
    }

    override suspend fun completeTask(task: Task) {
        val completedTask = Task(task.title, task.description, true, task.id)
        tasksServiceData[task.id] = completedTask
    }

    override suspend fun completeTask(taskId: String) {
        // Not required for the remote data source.
        throw NotImplementedError()
    }

    override suspend fun activateTask(task: Task) {
        val activeTask = Task(task.title, task.description, false, task.id)
        tasksServiceData[task.id] = activeTask
    }

    override suspend fun activateTask(taskId: String) {
        throw NotImplementedError()
    }

    override suspend fun clearCompletedTasks() {
        tasksServiceData = tasksServiceData.filterValues {
            !it.isCompleted
        } as LinkedHashMap<String, Task>
    }

    override suspend fun deleteTask(taskId: String) {
        tasksServiceData.remove(taskId)
        refreshTasks()
    }

    override suspend fun deleteAllTasks() {
        tasksServiceData.clear()
        refreshTasks()
    }

   
    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }
}

مرحله 4. سرویس دهنده خود را برای آزمایش آماده کنید

خوب ، زمان استفاده از ServiceLocator برای تعویض در تست در هنگام آزمایش. برای انجام این کار ، باید مقداری کد را به کد ServiceLocator خود اضافه کنید.

  1. Open ServiceLocator.kt .
  2. تنظیم کننده tasksRepository را به عنوان @VisibleForTesting علامت گذاری کنید. این حاشیه نویسی راهی برای بیان این است که دلیل عمومی بودن تنظیم کننده به دلیل آزمایش است.

servicelocator.kt

    @Volatile
    var tasksRepository: TasksRepository? = null
        @VisibleForTesting set

چه تست خود را به تنهایی یا در گروهی از تست ها اجرا کنید ، تست های شما باید دقیقاً یکسان باشد. این بدان معنی است که تست های شما نباید رفتاری داشته باشند که به یکدیگر وابسته باشد (این به معنای جلوگیری از به اشتراک گذاری اشیاء بین تست ها است).

از آنجا که ServiceLocator یک مجرد است ، این امکان را دارد که به طور تصادفی بین آزمایشات به اشتراک گذاشته شود. برای جلوگیری از این امر ، روشی را ایجاد کنید که به درستی حالت ServiceLocator بین تست ها را مجدداً تنظیم کند.

  1. یک متغیر نمونه به نام lock با Any مقدار اضافه کنید.

servicelocator.kt

private val lock = Any()
  1. یک روش خاص آزمایش به نام resetRepository را اضافه کنید که پایگاه داده را پاک می کند و هم مخزن و هم پایگاه داده را به NULL تنظیم می کند.

servicelocator.kt

    @VisibleForTesting
    fun resetRepository() {
        synchronized(lock) {
            runBlocking {
                TasksRemoteDataSource.deleteAllTasks()
            }
            // Clear all data to avoid test pollution.
            database?.apply {
                clearAllTables()
                close()
            }
            database = null
            tasksRepository = null
        }
    }

مرحله 5 از سرویس دهنده خود استفاده کنید

در این مرحله از ServiceLocator استفاده می کنید.

  1. TaskDetailFragmentTest را باز کنید .
  2. متغیر lateinit TasksRepository اعلام کنید.
  3. قبل از هر آزمایش ، یک تنظیم و یک روش پاره کردن را برای تنظیم یک FakeAndroidTestRepository اضافه کرده و بعد از هر آزمایش آن را تمیز کنید.

TaskDetailFragmentTest.kt

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. بدنه عملکرد activeTaskDetails_DisplayedInUi() را در runBlockingTest بپیچانید.
  2. قبل از راه اندازی این قطعه ، activeTask در مخزن ذخیره کنید.
repository.saveTask(activeTask)

آزمون نهایی مانند این کد در زیر به نظر می رسد.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }
  1. کل کلاس را با @ExperimentalCoroutinesApi حاشیه نویسی کنید.

پس از اتمام ، کد به این شکل خواهد بود.

TaskDetailFragmentTest.kt

@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }


    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

}
  1. آزمایش activeTaskDetails_DisplayedInUi() را اجرا کنید.

دقیقاً مانند گذشته ، شما باید این قطعه را به جز این بار مشاهده کنید ، زیرا به درستی مخزن را تنظیم کرده اید ، اکنون اطلاعات کار را نشان می دهد.


در این مرحله از کتابخانه تست Espresso UI برای تکمیل اولین تست ادغام خود استفاده خواهید کرد. شما کد خود را ساختار داده اید تا بتوانید با ادعاهای مربوط به UI خود تست ها را اضافه کنید. برای انجام این کار ، از کتابخانه تست اسپرسو استفاده خواهید کرد.

اسپرسو به شما کمک می کند:

  • تعامل با نمای ، مانند کلیک بر روی دکمه ها ، کشویی یک نوار یا پیمایش به پایین صفحه.
  • ادعا کنید که دیدگاه های خاصی روی صفحه نمایش قرار دارند یا در حالت خاصی قرار دارند (مانند حاوی متن خاص ، یا اینکه یک کادر چک بررسی می شود و غیره).

مرحله 1. وابستگی درجه یک توجه داشته باشید

شما در حال حاضر وابستگی اصلی اسپرسو را خواهید داشت زیرا به طور پیش فرض در پروژه های Android گنجانده شده است.

app/build.gradle

dependencies {

  // ALREADY in your code
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
   
 // Other dependencies
}

androidx.test.espresso:espresso-core این وابستگی اصلی اسپرسو هنگام تهیه یک پروژه جدید Android به طور پیش فرض درج شده است. این شامل کد تست اساسی برای اکثر نمایش ها و اقدامات مربوط به آنها است.

مرحله 2 انیمیشن ها را خاموش کنید

تست های اسپرسو بر روی یک دستگاه واقعی اجرا می شوند و بنابراین تست های ابزار دقیق از نظر طبیعت هستند. یکی از موضوعاتی که ایجاد می شود انیمیشن ها است: اگر یک انیمیشن عقب بماند و سعی کنید اگر یک نمای روی صفحه باشد ، آزمایش کنید ، اما هنوز هم انیمیشن است ، اسپرسو می تواند به طور تصادفی یک آزمایش را شکست دهد. این می تواند آزمایش های اسپرسو پوسته پوسته شود.

برای آزمایش UI اسپرسو ، بهترین تمرین برای خاموش کردن انیمیشن ها (همچنین تست شما سریعتر اجرا می شود!):

  1. در دستگاه تست خود به تنظیمات> گزینه های توسعه دهنده بروید.
  2. این سه تنظیمات را غیرفعال کنید: مقیاس انیمیشن پنجره ، مقیاس انیمیشن انتقال و مقیاس مدت زمان انیماتور .

مرحله 3. به یک تست اسپرسو نگاه کنید

قبل از نوشتن تست اسپرسو ، به برخی از کد های اسپرسو نگاهی بیندازید.

onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))

آنچه این عبارت انجام می دهد ، یافتن نمای کادر با ID task_detail_complete_checkbox ، روی آن کلیک می کند ، سپس ادعا می کند که بررسی شده است.

اکثر اظهارات اسپرسو از چهار بخش تشکیل شده است:

1. روش اسپرسو استاتیک

onView

onView نمونه ای از یک روش اسپرسو استاتیک است که بیانیه اسپرسو را شروع می کند. onView یکی از رایج ترین موارد است ، اما گزینه های دیگری مانند onData وجود دارد.

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId نمونه ای از ViewMatcher است که با شناسه آن نمای می شود. تطبیق های دید دیگری وجود دارد که می توانید در اسناد و مدارک جستجو کنید.

3. مشاهده

perform(click())

روش perform که ViewAction می کند. یک ViewAction کاری است که می تواند به نمای انجام شود ، به عنوان مثال در اینجا ، روی نمای کلیک می کند.

4. ViewAssertion

check(matches(isChecked()))

check کدام یک از ViewAssertion را می گیرد. ViewAssertion S را در مورد نمایش بررسی یا ادعا می کند. رایج ترین ViewAssertion شما استفاده می کنید ادعای matches است. برای به پایان رساندن این ادعا ، از یک ViewMatcher دیگر استفاده کنید ، در این مورد isChecked .

توجه داشته باشید که شما همیشه با هر دو perform تماس نمی گیرید و در بیانیه اسپرسو check . شما می توانید بیانیه هایی داشته باشید که فقط با استفاده از check ادعا می کنند یا فقط با استفاده از perform یک ViewAction انجام می دهند.

  1. TaskDetailFragmentTest.kt را باز کنید .
  2. تست activeTaskDetails_DisplayedInUi را به روز کنید .

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
    }

در اینجا بیانیه های واردات ، در صورت لزوم:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
  1. همه چیز بعد از // THEN نظر از اسپرسو استفاده می کند. ساختار تست و استفاده از withId را بررسی کنید و بررسی کنید تا در مورد نحوه نگاه صفحه جزئیات ادعا کنید.
  2. آزمون را اجرا کنید و آن را تأیید کنید.

مرحله 4. اختیاری ، تست اسپرسو خود را بنویسید

حالا خودتان یک تست بنویسید.

  1. یک آزمایش جدید به نام completedTaskDetails_DisplayedInUi ایجاد کنید و این کد اسکلت را کپی کنید.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
       
        // WHEN - Details fragment launched to display task
        
        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
}
  1. با نگاهی به آزمون قبلی ، این تست را کامل کنید .
  2. پاس های آزمون را اجرا کنید و تأیید کنید.

تمام شده به پایان completedTaskDetails_DisplayedInUi باید مانند این کد باشد.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
        val completedTask = Task("Completed Task", "AndroidX Rocks", true)
        repository.saveTask(completedTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
    }

در این مرحله آخر ، شما می توانید نحوه آزمایش مؤلفه ناوبری را با استفاده از نوع دیگری از تست دو برابر به نام Mock و کتابخانه آزمایش Mockito یاد بگیرید.

در این CodeLab شما از یک تست دو برابر به نام جعلی استفاده کرده اید. تقلبی یکی از انواع مختلف دو برابر تست است. برای آزمایش مؤلفه ناوبری از کدام تست دو برابر باید استفاده کنید؟

در مورد چگونگی وقوع ناوبری فکر کنید. تصور کنید که یکی از وظایف موجود در TasksFragment را برای حرکت به صفحه جزئیات کار فشار دهید.

در اینجا کد در TasksFragment وجود دارد که هنگام فشار دادن به صفحه نمایش جزئیات کار می کند.

TaskSfragment.kt

private fun openTaskDetails(taskId: String) {
    val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
    findNavController().navigate(action)
}


ناوبری به دلیل تماس با روش navigate اتفاق می افتد. اگر نیاز به نوشتن یک بیانیه ادعا دارید ، راهی ساده برای آزمایش اینکه آیا به TaskDetailFragment حرکت کرده اید وجود ندارد. پیمایش یک اقدام پیچیده است که منجر به تغییر و تحول در حالت روشن یا تغییر حالت نمی شود ، فراتر از اولیه سازی TaskDetailFragment .

آنچه می توانید ادعا کنید این است که روش navigate با پارامتر عمل صحیح فراخوانی شده است. این دقیقاً همان کاری است که یک تست مسخره انجام می دهد - این بررسی می کند که آیا روش های خاص خوانده شده است یا خیر.

Mockito چارچوبی برای ساخت دو برابر تست است. در حالی که کلمه مسخره در API و نام استفاده می شود ، فقط برای ساختن مسخره نیست . همچنین می تواند خرد و جاسوسی ایجاد کند.

شما از Mockito برای ساختن یک NavigationController مسخره استفاده خواهید کرد که می تواند ادعا کند که روش پیمایش به درستی خوانده می شود.

مرحله 1. وابستگی های Gradle را اضافه کنید

  1. وابستگی های Gradle را اضافه کنید .

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"

    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" 

    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"



  • org.mockito:mockito-core این وابستگی مسخره است.
  • dexmaker-mockito این کتابخانه موظف است از Mockito در یک پروژه Android استفاده کند. Mockito باید در زمان اجرا کلاس تولید کند. در Android ، این کار با استفاده از کد Dex Byte انجام می شود ، بنابراین این کتابخانه Mockito را قادر می سازد تا در زمان اجرا در Android اشیاء تولید کند.
  • androidx.test.espresso:espresso-contrib این کتابخانه از مشارکتهای خارجی (از این رو نام) تشکیل شده است که حاوی کد آزمایش برای نماهای پیشرفته تر مانند DatePicker و RecyclerView است. همچنین شامل چک های دسترسی و کلاس به نام CountingIdlingResource است که بعداً تحت پوشش قرار می گیرد.

مرحله 2. ایجاد TaskSfragmentTest

  1. TasksFragment باز کردن.
  2. بر روی نام کلاس TasksFragment راست کلیک کرده و Generate را انتخاب کنید و سپس تست کنید . در مجموعه منبع AndroidTest تست ایجاد کنید.
  3. این کد را در TasksFragmentTest کپی کنید.

TaskSfragmentTest.kt

@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }

}

این کد شبیه به کد TaskDetailFragmentTest است که شما نوشتید. آن را تنظیم می کند و یک FakeAndroidTestRepository را پاره می کند. یک تست ناوبری اضافه کنید تا آزمایش کنید که وقتی روی یک کار در لیست کار کلیک می کنید ، شما را به سمت TaskDetailFragment صحیح می برد.

  1. تست clickTask_navigateToDetailFragmentOne اضافه کنید .

TaskSfragmentTest.kt

    @Test
    fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
        repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
        repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        
    }
  1. برای ایجاد مسخره از عملکرد mock Mockito استفاده کنید.

TaskSfragmentTest.kt

 val navController = mock(NavController::class.java)

برای مسخره کردن در Mockito ، در کلاس که می خواهید مسخره کنید عبور کنید.

در مرحله بعد ، شما باید NavController خود را با این قطعه مرتبط کنید. onFragment به شما امکان می دهد روش هایی را در مورد خود قطعه فراخوانی کنید.

  1. مسخره جدید خود را NavController قطعه قطعه کنید.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. کد را اضافه کنید تا روی مورد در RecyclerView که دارای متن "عنوان 1" است کلیک کنید.
// WHEN - Click on the first list item
        onView(withId(R.id.tasks_list))
            .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("TITLE1")), click()))

RecyclerViewActions بخشی از کتابخانه espresso-contrib است و به شما امکان می دهد اقدامات اسپرسو را در یک بازیافت انجام دهید.

  1. تأیید کنید که با استدلال صحیح ، navigate نامیده شد.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")

روش verify Mockito همان چیزی است که این مسئله را مسخره می کند - شما قادر به تأیید navController مسخره به نام یک روش خاص ( navigate ) با یک پارامتر ( actionTasksFragmentToTaskDetailFragment با شناسه "ID1") هستید.

تست کامل به این شکل است:

@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
    repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
    repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

    // GIVEN - On the home screen
    val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
    
                val navController = mock(NavController::class.java)
    scenario.onFragment {
        Navigation.setViewNavController(it.view!!, navController)
    }

    // WHEN - Click on the first list item
    onView(withId(R.id.tasks_list))
        .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
            hasDescendant(withText("TITLE1")), click()))


    // THEN - Verify that we navigate to the first detail screen
    verify(navController).navigate(
        TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
    )
}
  1. آزمون خود را اجرا کنید!

به طور خلاصه ، برای آزمایش ناوبری می توانید:

  1. برای ایجاد مسخره NavController از Mockito استفاده کنید.
  2. آن NavController مسخره را به قطعه وصل کنید.
  3. تأیید کنید که پیمایش با عمل صحیح و پارامتر (ها) فراخوانی شده است.

مرحله 3 اختیاری ، نوشتن clickaddtaskbutton_navigatetoaddeditfragment

برای دیدن اینکه آیا می توانید خودتان یک آزمایش ناوبری بنویسید ، این کار را امتحان کنید.

  1. تست clickAddTaskButton_navigateToAddEditFragment را بنویسید که بررسی می کند که اگر روی + fab کلیک کنید ، به سمت AddEditTaskFragment اضافه می کنید.

پاسخ در زیر آمده است.

TaskSfragmentTest.kt

    @Test
    fun clickAddTaskButton_navigateToAddEditFragment() {
        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        val navController = mock(NavController::class.java)
        scenario.onFragment {
            Navigation.setViewNavController(it.view!!, navController)
        }

        // WHEN - Click on the "+" button
        onView(withId(R.id.add_task_fab)).perform(click())

        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
                null, getApplicationContext<Context>().getString(R.string.add_task)
            )
        )
    }

برای دیدن تفاوت بین کدی که شروع کرده اید و کد نهایی ، اینجا را کلیک کنید.

برای بارگیری کد برای CodeLab تمام شده ، می توانید از دستور GIT در زیر استفاده کنید:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_2


از طرف دیگر می توانید مخزن را به عنوان یک فایل ZIP بارگیری کنید ، آن را از حالت فشرده خارج کنید و آن را در Android Studio باز کنید.

دانلود زیپ

این CodeLab نحوه تنظیم تزریق وابستگی دستی ، یک سرویس دهنده خدمات و نحوه استفاده از جعل و مسخره در برنامه های Android Kotlin خود را پوشش می دهد. به طور خاص:

  • آنچه می خواهید آزمایش کنید و استراتژی تست خود انواع آزمایشی را که می خواهید برای برنامه خود اجرا کنید تعیین کنید. تست های واحد متمرکز و سریع هستند. تست های ادغام تعامل بین بخش هایی از برنامه شما را تأیید می کند. تست های پایان به پایان ، ویژگی ها را تأیید می کنند ، بالاترین وفاداری را دارند ، اغلب مورد استفاده قرار می گیرند و ممکن است بیشتر طول بکشد.
  • معماری برنامه شما تأثیر می گذارد که آزمایش چقدر سخت است.
  • توسعه TDD یا تست محور یک استراتژی است که در آن ابتدا تست ها را می نویسید ، سپس این ویژگی را برای گذراندن تست ها ایجاد کنید.
  • برای جداسازی بخش هایی از برنامه خود برای آزمایش ، می توانید از دو برابر تست استفاده کنید. Test Double نسخه ای از کلاس است که به طور خاص برای آزمایش ساخته شده است. به عنوان مثال ، شما داده های دریافت داده از یک پایگاه داده یا اینترنت را جعلی می کنید.
  • از تزریق وابستگی برای جایگزینی یک کلاس واقعی با یک کلاس آزمایش استفاده کنید ، به عنوان مثال ، یک مخزن یا یک لایه شبکه.
  • برای راه اندازی اجزای UI از تست Nstrumented ( androidTest ) استفاده کنید.
  • هنگامی که نمی توانید از تزریق وابستگی سازنده استفاده کنید ، به عنوان مثال برای راه اندازی یک قطعه ، اغلب می توانید از یک سرویس دهنده خدمات استفاده کنید. الگوی سرویس یاب سرویس جایگزینی برای تزریق وابستگی است. این شامل ایجاد یک کلاس Singleton به نام "Service Locator" است که هدف آن فراهم کردن وابستگی ها ، چه برای کد منظم و چه برای آزمون است.

دوره بی ادبی:

مستندات توسعه دهنده اندروید:

ویدئوها:

دیگر:

برای پیوندها به سایر CodeLabs در این دوره ، به صفحه Advanced Android در Kotlin Codelabs Landing مراجعه کنید.