این کد لبه بخشی از دوره آموزشی Advanced Android in Kotlin است. اگر از طریق کدها به صورت متوالی کار کنید، بیشترین ارزش را از این دوره خواهید گرفت، اما اجباری نیست. همه کدهای دوره در صفحه فرود Android Advanced in Kotlin Codelabs فهرست شده اند.
مقدمه
این دومین کد لبه آزمایشی همه چیز در مورد آزمایش دوگانه است: زمان استفاده از آنها در اندروید، و نحوه پیاده سازی آنها با استفاده از تزریق وابستگی، الگوی یاب سرویس و کتابخانه ها. با انجام این کار، یاد می گیرید که چگونه بنویسید:
- تست های واحد مخزن
- تستهای یکپارچهسازی قطعات و viewmodel
- تست های ناوبری قطعه
آنچه از قبل باید بدانید
باید با:
- زبان برنامه نویسی کاتلین
- آزمایش مفاهیم پوشش داده شده در اولین کد: نوشتن و اجرای تست های واحد در اندروید، با استفاده از JUnit، Hamcrest، تست AndroidX، Robolectric، و همچنین تست LiveData
- کتابخانههای اصلی Android Jetpack:
ViewModel
،LiveData
و Navigation Component - معماری برنامه، از الگوی راهنمای معماری برنامه و کد لبه های Android Fundamentals پیروی می کند
- اصول اولیه کوروتین ها در اندروید
چیزی که یاد خواهید گرفت
- نحوه برنامه ریزی استراتژی تست
- نحوه ایجاد و استفاده از دوبل های آزمایشی، یعنی تقلبی و ساختگی
- نحوه استفاده از تزریق وابستگی دستی در اندروید برای تست های واحد و ادغام
- نحوه اعمال الگوی یاب سرویس
- نحوه تست مخازن، قطعات، مدل های مشاهده و مولفه 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 استفاده می کند. اگر با هر یک از نمونه های زیر آشنا هستید، این برنامه معماری مشابهی دارد:
- اتاق با نمای Codelab
- کد لبه های آموزشی Android Kotlin Fundamentals
- کد لبه های آموزش پیشرفته اندروید
- نمونه گل آفتابگردان اندروید
- دوره آموزشی توسعه اپلیکیشن اندروید با Kotlin Udacity
مهمتر است که معماری کلی برنامه را درک کنید تا اینکه درک عمیقی از منطق در هر لایه داشته باشید.
در اینجا خلاصه بسته هایی است که خواهید یافت:
بسته: | |
| صفحه افزودن یا ویرایش یک کار: کد لایه رابط کاربری برای افزودن یا ویرایش یک کار. |
| لایه داده: این لایه به لایه داده وظایف می پردازد. این شامل پایگاه داده، شبکه و کد مخزن است. |
| صفحه آمار: کد لایه رابط کاربری برای صفحه آمار. |
| صفحه جزئیات کار: کد لایه رابط کاربری برای یک کار واحد. |
| صفحه وظایف: کد لایه رابط کاربری برای لیست همه وظایف. |
| کلاسهای کاربردی : کلاسهای مشترک مورد استفاده در بخشهای مختلف برنامه، بهعنوان مثال برای طرحبندی بازخوانی کشیدن انگشتی که در چندین صفحه استفاده میشود. |
لایه داده (.data)
این برنامه شامل یک لایه شبکه شبیه سازی شده، در بسته راه دور ، و یک لایه پایگاه داده، در بسته محلی است. برای سادگی، در این پروژه لایه شبکه تنها با یک HashMap
با تاخیر شبیه سازی می شود تا درخواست های واقعی شبکه.
DefaultTasksRepository
بین لایه شبکه و لایه پایگاه داده مختصات یا واسطه می شود و همان چیزی است که داده ها را به لایه UI برمی گرداند.
لایه رابط کاربری (.addedittask، .statistics، .taskdetail، .tasks)
هر یک از بسته های لایه UI شامل یک قطعه و یک مدل view به همراه هر کلاس دیگری است که برای UI مورد نیاز است (مانند یک آداپتور برای لیست وظایف). TaskActivity
است که شامل تمام قطعات است.
جهت یابی
ناوبری برای برنامه توسط مؤلفه ناوبری کنترل می شود . در فایل nav_graph.xml
تعریف شده است. ناوبری در مدل های view با استفاده از کلاس Event
فعال می شود. مدلهای view نیز تعیین میکنند که چه آرگومانهایی باید منتقل شوند. قطعات Event
را مشاهده میکنند و پیمایش واقعی بین صفحهها را انجام میدهند.
در این کد لبه، نحوه تست مخازن، مشاهده مدل ها و قطعات با استفاده از دو برابر و تزریق وابستگی را یاد خواهید گرفت. قبل از اینکه در مورد این تست ها غوطه ور شوید، مهم است که استدلالی را که راهنمایی می کند این تست ها را چگونه و چگونه بنویسید، درک کنید.
این بخش برخی از بهترین روشهای آزمایش را به طور کلی پوشش میدهد، زیرا برای Android اعمال میشود.
هرم تست
وقتی در مورد استراتژی تست فکر می کنیم، سه جنبه تست مرتبط وجود دارد:
- محدوده — تست چه مقدار از کد را لمس می کند؟ آزمایشها میتوانند روی یک روش واحد، در کل برنامه یا جایی در میان اجرا شوند.
- سرعت — تست با چه سرعتی اجرا می شود؟ سرعت تست می تواند از میلی ثانیه تا چند دقیقه متفاوت باشد.
- وفاداری — آزمون چقدر «دنیای واقعی» است؟ به عنوان مثال، اگر بخشی از کدی که در حال آزمایش آن هستید نیاز به درخواست شبکه داشته باشد، آیا کد آزمایشی واقعاً این درخواست شبکه را انجام می دهد یا نتیجه را جعلی می کند؟ اگر آزمایش واقعاً با شبکه صحبت می کند، به این معنی است که وفاداری بالاتری دارد. معاوضه این است که اجرای آزمایش ممکن است بیشتر طول بکشد، در صورت قطع شدن شبکه ممکن است منجر به خطا شود یا استفاده از آن پرهزینه باشد.
مبادلات ذاتی بین این جنبه ها وجود دارد. به عنوان مثال، سرعت و وفاداری یک مبادله هستند - هر چه تست سریعتر باشد، به طور کلی، وفاداری کمتر، و بالعکس. یکی از روش های رایج برای تقسیم تست های خودکار به این سه دسته است:
- تست های واحد — این تست ها بسیار متمرکز هستند که روی یک کلاس اجرا می شوند، معمولاً یک متد در آن کلاس. اگر تست واحد ناموفق باشد، میتوانید دقیقاً بدانید که مشکل در کجای کدتان قرار دارد. آنها وفاداری پایینی دارند زیرا در دنیای واقعی، برنامه شما بسیار بیشتر از اجرای یک متد یا کلاس است. آنها به اندازه ای سریع هستند که هر بار که کد خود را تغییر می دهید اجرا شوند. آنها اغلب به صورت محلی (در مجموعه منبع
test
) اجرا می شوند. مثال: تست تک روش ها در مدل های view و مخازن. - تستهای یکپارچهسازی : این تستها تعامل چندین کلاس را آزمایش میکنند تا مطمئن شوند که هنگام استفاده با هم مطابق انتظار رفتار میکنند. یکی از راههای ساختاربندی تستهای یکپارچهسازی این است که آنها یک ویژگی واحد را آزمایش کنند، مانند توانایی ذخیره یک کار. آنها محدوده وسیع تری از کد را نسبت به تست های واحد آزمایش می کنند، اما همچنان برای اجرای سریع، در مقابل وفاداری کامل، بهینه شده اند. بسته به شرایط می توان آنها را به صورت محلی یا به عنوان تست ابزار دقیق اجرا کرد. مثال: آزمایش تمام عملکردهای یک جفت مدل قطعه و view.
- تست های پایان به انتها (E2e) - ترکیبی از ویژگی ها را آزمایش کنید که با هم کار می کنند. آنها بخشهای بزرگی از برنامه را آزمایش میکنند، استفاده واقعی را از نزدیک شبیهسازی میکنند و بنابراین معمولاً کند هستند. آنها بالاترین وفاداری را دارند و به شما می گویند که برنامه شما در واقع به طور کلی کار می کند. به طور کلی، این تست ها تست های ابزاری خواهند بود (در مجموعه منبع
androidTest
)
مثال: راه اندازی کل برنامه و آزمایش چند ویژگی با هم.
نسبت پیشنهادی این تستها اغلب توسط یک هرم نشان داده میشود، که اکثریت قریب به اتفاق تستها تستهای واحد هستند.
معماری و تست
توانایی شما برای آزمایش برنامه خود در تمام سطوح مختلف هرم آزمایشی به طور ذاتی با معماری برنامه شما مرتبط است. به عنوان مثال، یک برنامه کاربردی با معماری بسیار ضعیف ممکن است تمام منطق خود را در یک متد قرار دهد. ممکن است بتوانید یک تست پایان به انتها برای این کار بنویسید، زیرا این تستها تمایل دارند بخشهای بزرگی از برنامه را آزمایش کنند، اما در مورد تستهای واحد نوشتن یا یکپارچهسازی چطور؟ با وجود همه کدها در یک مکان، آزمایش فقط کد مربوط به یک واحد یا ویژگی دشوار است.
یک رویکرد بهتر این است که منطق برنامه را به چندین روش و کلاس تقسیم کنیم و به هر قطعه اجازه دهیم به صورت مجزا آزمایش شود. معماری راهی برای تقسیم و سازماندهی کد شماست که امکان تست واحد و یکپارچه سازی آسان تر را فراهم می کند. برنامه TO-DO که آزمایش می کنید از معماری خاصی پیروی می کند:
در این درس، نحوه تست کردن بخش هایی از معماری فوق را به صورت مجزا مشاهده خواهید کرد:
- ابتدا مخزن را واحد تست خواهید کرد.
- سپس از یک تست دوبل در مدل view استفاده می کنید که برای تست واحد و تست یکپارچه سازی مدل view ضروری است.
- در مرحله بعد، نوشتن تستهای یکپارچهسازی برای قطعات و مدلهای نمای آنها را یاد خواهید گرفت.
- در نهایت، نوشتن تستهای یکپارچهسازی را یاد خواهید گرفت که شامل مولفه 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 برای دریافت یک محیط اندروید شبیه سازی شده استفاده کنید.
- برخی از بخشهای کد، مانند کد شبکه، ممکن است زمان زیادی طول بکشد تا اجرا شود، یا حتی گاهی اوقات با شکست مواجه میشود و آزمایشهای طولانیمدت و پوستهپوستی ایجاد میکند.
- آزمایشهای شما ممکن است توانایی خود را برای تشخیص اینکه کدام کد مقصر خطای آزمایش است، از دست بدهند. تستهای شما میتوانند شروع به آزمایش کد غیر مخزن کنند، بنابراین، برای مثال، آزمایشهای واحد "مخزن" فرضی شما ممکن است به دلیل مشکل در برخی از کدهای وابسته، مانند کد پایگاه داده، با شکست مواجه شوند.
تست دونفره
راه حل این است که وقتی در حال آزمایش مخزن هستید، از کد شبکه یا پایگاه داده واقعی استفاده نکنید ، بلکه از یک تست دوبل استفاده کنید. تست دو نسخه ای از یک کلاس است که به طور خاص برای آزمایش ساخته شده است. این به معنای جایگزینی نسخه واقعی یک کلاس در تست ها است. شبیه این است که یک بدلکار بازیگری است که در بدلکاری تخصص دارد و بازیگر واقعی را برای اقدامات خطرناک جایگزین می کند.
در اینجا چند نوع تست دوبل آورده شده است:
جعلی | یک تست دوتایی که دارای یک پیادهسازی "در حال کار" از کلاس است، اما به گونهای پیادهسازی شده است که برای آزمایشها خوب است اما برای تولید نامناسب است. |
مسخره کردن | یک تست دوبل که ردیابی می کند کدام یک از متدهای آن فراخوانی شده است. سپس بسته به اینکه روشهای آن به درستی فراخوانی شده باشند، در یک آزمون موفق میشود یا ناموفق میشود. |
خرد | یک تست دوبل که شامل هیچ منطقی نمی شود و فقط آنچه را که برنامه ریزی کرده اید برمی گرداند. یک |
ساختگی | یک تست دوتایی که در اطراف ارسال می شود اما استفاده نمی شود، مثلاً اگر فقط باید آن را به عنوان یک پارامتر ارائه کنید. اگر |
جاسوس | یک تست دوبل که همچنین برخی از اطلاعات اضافی را ردیابی می کند. برای مثال، اگر یک |
برای کسب اطلاعات بیشتر در مورد تست های دوتایی، تست در توالت: دوبل های تست خود را بشناسید .
رایج ترین تست های دوگانه مورد استفاده در اندروید Fakes و Mocks هستند.
در این کار، شما یک FakeDataSource
تست دو تا واحدی DefaultTasksRepository
جدا از منابع داده واقعی ایجاد می کنید.
مرحله 1: کلاس FakeDataSource را ایجاد کنید
در این مرحله شما می خواهید کلاسی به نام FakeDataSouce
ایجاد کنید که یک تست دوگانه از LocalDataSource
و RemoteDataSource
خواهد بود.
- در مجموعه منبع آزمایشی ، روی New -> Package کلیک راست کنید.
- یک بسته داده با یک بسته منبع در داخل بسازید.
- یک کلاس جدید به نام
FakeDataSource
در بسته data/source ایجاد کنید.
مرحله 2: رابط TasksDataSource را پیاده سازی کنید
برای اینکه بتوانید از کلاس جدید FakeDataSource
خود به عنوان دو تست استفاده کنید، باید بتواند جایگزین سایر منابع داده شود. این منابع داده عبارتند از TasksLocalDataSource
و TasksRemoteDataSource
.
- توجه کنید که هر دوی اینها چگونه رابط
TasksDataSource
را پیاده سازی می کنند.
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }
- کاری کنید که
FakeDataSource
را پیاده سازیTasksDataSource
:
class FakeDataSource : TasksDataSource {
}
Android Studio شکایت خواهد کرد که روشهای لازم را برای TasksDataSource
.
- از منوی رفع سریع استفاده کنید و Implement Members را انتخاب کنید.
- همه روش ها را انتخاب کرده و OK را فشار دهید.
مرحله 3: متد getTasks را در FakeDataSource پیاده سازی کنید
FakeDataSource
نوع خاصی از تست دوگانه است که به آن Fake گفته می شود. جعلی یک آزمایش دوگانه است که دارای یک پیادهسازی «کار» کلاس است، اما به گونهای پیادهسازی شده است که برای آزمایش خوب است اما برای تولید نامناسب است. پیاده سازی "کار" به این معنی است که کلاس خروجی های واقعی را با ورودی های داده شده تولید می کند.
برای مثال، منبع داده جعلی شما به شبکه متصل نمیشود یا چیزی را در پایگاه داده ذخیره نمیکند، بلکه فقط از یک لیست درون حافظه استفاده میکند. این "همانطور که ممکن است انتظار داشته باشید" عمل می کند زیرا روش های دریافت یا ذخیره وظایف نتایج مورد انتظار را نشان می دهد، اما هرگز نمی توانید از این پیاده سازی در تولید استفاده کنید، زیرا در سرور یا پایگاه داده ذخیره نمی شود.
یک FakeDataSource
- به شما امکان می دهد کد را در
DefaultTasksRepository
بدون نیاز به تکیه بر پایگاه داده یا شبکه واقعی آزمایش کنید. - یک پیاده سازی "به اندازه کافی واقعی" برای آزمایش ها ارائه می دهد.
- سازنده
FakeDataSource
را برای ایجاد یکvar
به نامtasks
تغییر دهید کهMutableList<Task>?
با مقدار پیش فرض یک لیست خالی قابل تغییر.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
این لیستی از وظایفی است که به عنوان یک پایگاه داده یا پاسخ سرور "جعل" می شوند. در حال حاضر، هدف آزمایش روش getTasks
مخزن است . این روشهای getTasks
، deleteAllTasks
و saveTask
منبع داده را فراخوانی میکند.
یک نسخه جعلی از این روش ها بنویسید:
-
getTasks
را بنویسید: اگرtasks
null
نیستند، یک نتیجهSuccess
را برگردانید. اگرtasks
null
است، یک نتیجهError
را برگردانید. -
deleteAllTasks
بنویسید: لیست وظایف قابل تغییر را پاک کنید. -
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 استفاده کنید
- سازنده
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 }
- چون وابستگی ها را وارد کردید، متد
init
را حذف کنید. دیگر نیازی به ایجاد وابستگی ندارید. - همچنین متغیرهای نمونه قدیمی را حذف کنید. شما آنها را در سازنده تعریف می کنید:
DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
- در نهایت، متد
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
خود استفاده کنید.
- روی نام کلاس
DefaultTasksRepository
کلیک راست کرده و Generate و سپس Test را انتخاب کنید. - دستورات را برای ایجاد
DefaultTasksRepositoryTest
در مجموعه منبع آزمایشی دنبال کنید. - در بالای کلاس
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 }
- سه متغیر، دو متغیر عضو
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
استفاده خواهد کرد.
- روشی به نام
createRepository
و آن را با@Before
حاشیه نویسی کنید. - منابع داده جعلی خود را با استفاده از لیست های
remoteTasks
وlocalTasks
کنید. -
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
است!
- یک تست برای متد
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 را برای مدیریت راهاندازی کوروتینها در تستهای خود اضافه کنید.
- با استفاده از
testImplementation
، وابستگی های مورد نیاز برای تست کوروتین ها را به مجموعه منبع تست اضافه کنید.
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
همگام سازی را فراموش نکنید!
kotlinx-coroutines-test
test کتابخانه تست coroutines است که به طور خاص برای آزمایش کوروتین ها در نظر گرفته شده است. برای اجرای تست های خود، از تابع runBlockingTest
استفاده کنید. این تابعی است که توسط کتابخانه تست coroutines ارائه شده است. یک بلوک از کد را می گیرد و سپس این بلوک کد را در یک زمینه ویژه کاری اجرا می کند که به صورت همزمان و بلافاصله اجرا می شود، به این معنی که اقدامات به ترتیب قطعی انجام می شود. این اساساً باعث میشود که کوروتینهای شما مانند موارد غیرکوروتین اجرا شوند، بنابراین برای آزمایش کد در نظر گرفته شده است.
هنگامی که یک تابع suspend
فراخوانی می کنید، از runBlockingTest
در کلاس های آزمایشی خود استفاده کنید. شما در مورد نحوه عملکرد runBlockingTest
و نحوه آزمایش کوروتین ها در کدهای بعدی این سری اطلاعات بیشتری کسب خواهید کرد.
-
@ExperimentalCoroutinesApi
را بالای کلاس اضافه کنید. این نشان میدهد که میدانید از یک api کوروتین آزمایشی (runBlockingTest
) در کلاس استفاده میکنید. بدون آن، شما یک هشدار دریافت خواهید کرد. - به
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))
}
}
- تست
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
باشد.
-
DefaultTasksRepository
را باز کرده و روی نام کلاس کلیک راست کنید . سپس Refactor -> Extract -> Interface را انتخاب کنید.
- برای جداسازی فایل Extract را انتخاب کنید.
- در پنجره Extract Interface ، نام رابط را به
TasksRepository
تغییر دهید. - در قسمت Members to form interface ، همه اعضا به جز دو عضو همراه و متدهای خصوصی را بررسی کنید.
- روی Refactor کلیک کنید. رابط
TasksRepository
جدید باید در بسته داده/منبع ظاهر شود.
و DefaultTasksRepository
اکنون TasksRepository
را پیاده سازی می کند.
- برنامه خود را اجرا کنید (نه تست ها) تا مطمئن شوید که همه چیز هنوز در حالت کار است.
مرحله 2. FakeTestRepository را ایجاد کنید
اکنون که رابط را در اختیار دارید، می توانید دو تست DefaultTaskRepository
را ایجاد کنید.
- در مجموعه منبع آزمایشی ، در data/source فایل Kotlin و کلاس
FakeTestRepository.kt
را ایجاد کنید و از رابطTasksRepository
گسترش دهید.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
به شما گفته می شود که باید متدهای رابط را پیاده سازی کنید.
- نشانگر را روی خطا نگه دارید تا منوی پیشنهاد را مشاهده کنید، سپس کلیک کرده و Implement Members را انتخاب کنید.
- همه روش ها را انتخاب کرده و OK را فشار دهید.
مرحله 3. روش های FakeTestRepository را پیاده سازی کنید
اکنون یک کلاس FakeTestRepository
با متدهای "نیست اجرا شده" دارید. مشابه نحوه اجرای FakeDataSource
، FakeTestRepository
به جای پرداختن به میانجیگری پیچیده بین منابع داده محلی و راه دور، توسط یک ساختار داده پشتیبانی می شود.
توجه داشته باشید که FakeTestRepository
شما نیازی به استفاده از FakeDataSource
یا هر چیز دیگری ندارد. فقط باید خروجی های جعلی واقع بینانه با ورودی های داده شده را برگرداند. شما از LinkedHashMap
برای ذخیره لیست وظایف و MutableLiveData
برای وظایف قابل مشاهده خود استفاده خواهید کرد.
- در
FakeTestRepository
، هم یک متغیرLinkedHashMap
که لیست فعلی وظایف را نشان می دهد و هم یکMutableLiveData
را برای وظایف قابل مشاهده خود اضافه کنید.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
// Rest of class
}
روش های زیر را اجرا کنید:
-
getTasks
— این روش بایدtasksServiceData
را گرفته و با استفاده ازtasksServiceData.values.toList()
آن را به لیست تبدیل کند و سپس آن را به عنوان نتیجهSuccess
برگرداند. -
refreshTasks
- مقدارobservableTasks
را بهروزرسانی میکند تا همان چیزی باشد که توسطgetTasks()
برگردانده میشود. - ObserverTasks - با استفاده از
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
تماس بگیرید، اما برای آسانتر کردن این کار، یک روش کمکی مخصوص آزمایشها اضافه کنید که به شما امکان میدهد وظایف را اضافه کنید.
- متد
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
شروع می کنید.
-
TasksViewModel
باز کنید. - سازنده
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
قرار دهید، اما می توانید آن را در فایل خودش نیز قرار دهید.
- در پایین فایل
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 خود را می سازید استفاده کنید.
- برای استفاده از کارخانه،
TasksFragment
را به روز کنید.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TasksViewModel>()
// WITH
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- کد برنامه خود را اجرا کنید و مطمئن شوید که همه چیز همچنان کار می کند!
مرحله 2. از FakeTestRepository در TasksViewModelTest استفاده کنید
اکنون به جای استفاده از مخزن واقعی در تست های مدل view خود، می توانید از مخزن جعلی استفاده کنید.
-
TasksViewModelTest
را باز کنید. - یک ویژگی
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
}
- متد
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)
}
- از آنجایی که دیگر از کد AndroidX Test
ApplicationProvider.getApplicationContext
استفاده نمی کنید، می توانید حاشیه نویسی@RunWith(AndroidJUnit4::class)
را نیز حذف کنید. - تست های خود را اجرا کنید، مطمئن شوید که همه آنها هنوز کار می کنند!
By using constructor dependency injection, you've now removed the DefaultTasksRepository
as a dependency and replaced it with your FakeTestRepository
in the tests.
Step 3. Also Update TaskDetail Fragment and ViewModel
Make the exact same changes for the TaskDetailFragment
and TaskDetailViewModel
. This will prepare the code for when you write TaskDetail
tests next.
- Open
TaskDetailViewModel
. - Update the constructor:
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 }
- At the bottom of the
TaskDetailViewModel
file, outside the class, add aTaskDetailViewModelFactory
.
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)
}
- Update
TasksFragment
to use the factory.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()
// WITH
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- Run your code and make sure everything is working.
You are now able to use a FakeTestRepository
instead of the real repository in TasksFragment
and TasksDetailFragment
.
Next you'll write integration tests to test your fragment and view-model interactions. You'll find out if your view model code appropriately updates your UI. To do this you use
- the ServiceLocator pattern
- the Espresso and Mockito libraries
Integration tests test the interaction of several classes to make sure they behave as expected when used together. These tests can be run either locally ( test
source set) or as instrumentation tests ( androidTest
source set).
In your case you'll be taking each fragment and writing integration tests for the fragment and view model to test the main features of the fragment.
Step 1. Add Gradle Dependencies
- Add the following gradle dependencies.
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"
These dependencies include:
-
junit:junit
—JUnit, which is necessary for writing basic test statements. -
androidx.test:core
—Core AndroidX test library -
kotlinx-coroutines-test
—The coroutines testing library -
androidx.fragment:fragment-testing
—AndroidX test library for creating fragments in tests and changing their state.
Since you'll be using these libraries in your androidTest
source set, use androidTestImplementation
to add them as dependencies.
Step 2. Make a TaskDetailFragmentTest class
The TaskDetailFragment
shows information about a single task.
You'll start by writing a fragment test for the TaskDetailFragment
since it has fairly basic functionality compared to the other fragments.
- Open
taskdetail.TaskDetailFragment
. - Generate a test for
TaskDetailFragment
, as you've done before. Accept the default choices and put it in the androidTest source set (NOT thetest
source set).
- Add the following annotations to the
TaskDetailFragmentTest
class.
TaskDetailFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}
The purpose of these annotation is:
-
@MediumTest
—Marks the test as a "medium run-time" integration test (versus@SmallTest
unit tests and@LargeTest
large end-to-end tests). This helps you group and choose which size of test to run. -
@RunWith(AndroidJUnit4::class)
—Used in any class using AndroidX Test.
Step 3. Launch a fragment from a test
In this task, you're going to launch TaskDetailFragment
using the AndroidX Testing library . FragmentScenario
is a class from AndroidX Test that wraps around a fragment and gives you direct control over the fragment's lifecycle for testing. To write tests for fragments, you create a FragmentScenario
for the fragment you're testing ( TaskDetailFragment
).
- Copy this test into
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)
}
This code above:
- Creates a task.
- Creates a
Bundle
, which represents the fragment arguments for the task that get passed into the fragment). - The
launchFragmentInContainer
function creates aFragmentScenario
, with this bundle and a theme.
This is not a finished test yet, because it's not asserting anything. For now, run the test and observe what happens.
- This is an instrumented test, so make sure the emulator or your device is visible.
- Run the test.
A few things should happen.
- First, because this is an instrumented test, the test will run on either your physical device (if connected) or an emulator.
- It should launch the fragment.
- Notice how it doesn't navigate through any other fragment or have any menus associated with the activity - it is just the fragment.
Finally, look closely and notice that the fragment says "No data" as it doesn't successfully load up the task data.
Your test both needs to load up the TaskDetailFragment
(which you've done) and assert the data was loaded correctly. Why is there no data? This is because you created a task, but you didn't save it to the repository.
@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)
}
You have this FakeTestRepository
, but you need some way to replace your real repository with your fake one for your fragment . You'll do this next!
In this task, you'll provide your fake repository to your fragment using a ServiceLocator
. This will allow you to write your fragment and view model integration tests.
You can't use constructor dependency injection here, as you did before, when you needed to provide a dependency to the view model or repository. Constructor dependency injection requires that you construct the class. Fragments and activities are examples of classes that you don't construct and generally don't have access to the constructor of.
Since you don't construct the fragment, you can't use constructor dependency injection to swap the repository test double ( FakeTestRepository
) to the fragment. Instead, use the Service Locator pattern. The Service Locator pattern is an alternative to Dependency Injection. It involves creating a singleton class called the "Service Locator", whose purpose is to provide dependencies, both for the regular and test code. In the regular app code (the main
source set), all of these dependencies are the regular app dependencies. For the tests, you modify the Service Locator to provide test double versions of the dependencies.
Not using Service Locator | Using a Service Locator |
For this codelab app, do the following:
- Create a Service Locator class that is able to construct and store a repository. By default it constructs a "normal" repository.
- Refactor your code so that when you need a repository, use the Service Locator.
- In your testing class, call a method on the Service Locator which swaps out the "normal" repository with your test double.
Step 1. Create the ServiceLocator
Let's make a ServiceLocator
class. It'll live in the main source set with the rest of the app code because it's used by the main application code.
Note: The ServiceLocator
is a singleton, so use the Kotlin object
keyword for the class.
- Create the file ServiceLocator.kt in the top level of the main source set.
- Define an
object
calledServiceLocator
. - Create
database
andrepository
instance variables and set both tonull
. - Annotate the repository with
@Volatile
because it could get used by multiple threads (@Volatile
is explained in detail here ).
Your code should look as a shown below.
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}
Right now the only thing your ServiceLocator
needs to do is know how to return a TasksRepository
. It'll return a pre-existing DefaultTasksRepository
or make and return a new DefaultTasksRepository
, if needed.
Define the following functions:
-
provideTasksRepository
—Either provides an already existing repository or creates a new one. This method should besynchronized
onthis
to avoid, in situations with multiple threads running, ever accidentally creating two repository instances. -
createTasksRepository
—Code for creating a new repository. Will callcreateTaskLocalDataSource
and create a newTasksRemoteDataSource
. -
createTaskLocalDataSource
—Code for creating a new local data source. Will callcreateDataBase
. -
createDataBase
—Code for creating a new database.
The completed code is below.
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
}
}
Step 2. Use ServiceLocator in Application
You're going to make a change to your main application code (not your tests) so that you create the repository in one place, your ServiceLocator
.
It's important that you only ever make one instance of the repository class. To ensure this, you'll use the Service locator in my Application class.
- At the top level of your package hierarchy, open
TodoApplication
and create aval
for your repository and assign it a repository that is obtained usingServiceLocator.provideTaskRepository
.
TodoApplication.kt
class TodoApplication : Application() {
val taskRepository: TasksRepository
get() = ServiceLocator.provideTasksRepository(this)
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(DebugTree())
}
}
Now that you have created a repository in the application, you can remove the old getRepository
method in DefaultTasksRepository
.
- Open
DefaultTasksRepository
and delete the companion object.
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
}
}
}
}
Now everywhere you were using getRepository
, use the application's taskRepository
instead. This ensures that instead of making the repository directly, you are getting whatever repository the ServiceLocator
provided.
- Open
TaskDetailFragement
and find the call togetRepository
at the top of the class. - Replace this call with a call that gets the repository from
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)
}
- Do the same for
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)
}
- For
StatisticsViewModel
andAddEditTaskViewModel
, update the code that acquires the repository to use the repository from theTodoApplication
.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- Run your application (not the test)!
Since you only refactored, the app should run the same without issue.
Step 3. Create FakeAndroidTestRepository
You already have a FakeTestRepository
in the test source set. You cannot share test classes between the test
and androidTest
source sets by default. So, you need to make a duplicate FakeTestRepository
class in the androidTest
source set, and call it FakeAndroidTestRepository
.
- Right-click the
androidTest
source set and make a data package. Right-click again and make a source package. - Make a new class in this source package called
FakeAndroidTestRepository.kt
. - Copy the following code to that class.
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() }
}
}
Step 4. Prepare your ServiceLocator for Tests
Okay, time to use the ServiceLocator
to swap in test doubles when testing. To do that, you need to add some code to your ServiceLocator
code.
- Open
ServiceLocator.kt
. - Mark the setter for
tasksRepository
as@VisibleForTesting
. This annotation is a way to express that the reason the setter is public is because of testing.
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
Whether you run your test alone or in a group of tests, your tests should run exactly the same. What this means is that your tests should have no behavior that is dependent on one another (which means avoiding sharing objects between tests).
Since the ServiceLocator
is a singleton, it has the possibility of being accidentally shared between tests. To help avoid this, create a method that properly resets the ServiceLocator
state between tests.
- Add an instance variable called
lock
with theAny
value.
ServiceLocator.kt
private val lock = Any()
- Add a testing-specific method called
resetRepository
which clears out the database and sets both the repository and database to 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
}
}
Step 5. Use your ServiceLocator
In this step, you use the ServiceLocator
.
- Open
TaskDetailFragmentTest
. - Declare a
lateinit TasksRepository
variable. - Add a setup and a tear down method to set up a
FakeAndroidTestRepository
before each test and clean it up after each test.
TaskDetailFragmentTest.kt
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
- Wrap the function body of
activeTaskDetails_DisplayedInUi()
inrunBlockingTest
. - Save
activeTask
in the repository before launching the fragment.
repository.saveTask(activeTask)
The final test looks like this code below.
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)
}
- Annotate the whole class with
@ExperimentalCoroutinesApi
.
When finished, the code will look like this.
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)
}
}
- Run the
activeTaskDetails_DisplayedInUi()
test.
Much like before, you should see the fragment, except this time, because you properly set up the repository, it now shows the task information.
In this step, you'll use the Espresso UI testing library to complete your first integration test. You have structured your code so you can add tests with assertions for your UI. To do that, you'll use the Espresso testing library .
Espresso helps you:
- Interact with views, like clicking buttons, sliding a bar, or scrolling down a screen.
- Assert that certain views are on screen or are in a certain state (such as containing particular text, or that a checkbox is checked, etc.).
Step 1. Note Gradle Dependency
You'll already have the main Espresso dependency since it is included in Android projects by default.
app/build.gradle
dependencies {
// ALREADY in your code
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
// Other dependencies
}
androidx.test.espresso:espresso-core
—This core Espresso dependency is included by default when you make a new Android project. It contains the basic testing code for most views and actions on them.
Step 2. Turn off animations
Espresso tests run on a real device and thus are instrumentation tests by nature. One issue that arises is animations: If an animation lags and you try to test if a view is on screen, but it's still animating, Espresso can accidentally fail a test. This can make Espresso tests flaky.
For Espresso UI testing, it's best practice to turn animations off (also your test will run faster!):
- On your testing device, go to Settings > Developer options .
- Disable these three settings: Window animation scale , Transition animation scale , and Animator duration scale .
Step 3. Look at an Espresso test
Before you write an Espresso test, take a look at some Espresso code.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))
What this statement does is find the checkbox view with the id task_detail_complete_checkbox
, clicks it, then asserts that it is checked.
The majority of Espresso statements are made up of four parts:
onView
onView
is an example of a static Espresso method that starts an Espresso statement. onView
is one of the most common ones, but there are other options, such as onData
.
2. ViewMatcher
withId(R.id.task_detail_title_text)
withId
is an example of a ViewMatcher
which gets a view by its ID. There are other view matchers which you can look up in the documentation .
3. ViewAction
perform(click())
The perform
method which takes a ViewAction
. A ViewAction
is something that can be done to the view, for example here, it's clicking the view.
check(matches(isChecked()))
check
which takes a ViewAssertion
. ViewAssertion
s check or asserts something about the view. The most common ViewAssertion
you'll use is the matches
assertion. To finish the assertion, use another ViewMatcher
, in this case isChecked
.
Note that you don't always call both perform
and check
in an Espresso statement. You can have statements that just make an assertion using check
or just do a ViewAction
using perform
.
- Open
TaskDetailFragmentTest.kt
. - Update the
activeTaskDetails_DisplayedInUi
test.
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())))
}
Here are the import statements, if needed:
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
- Everything after the
// THEN
comment uses Espresso. Examine the test structure and the use ofwithId
and check to make assertions about how the detail page should look. - Run the test and confirm it passes.
Step 4. Optional, Write your own Espresso Test
Now write a test yourself.
- Create a new test called
completedTaskDetails_DisplayedInUi
and copy this skeleton code.
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
}
- Looking at the previous test, complete this test.
- Run and confirm the test passes.
The finished completedTaskDetails_DisplayedInUi
should look like this code.
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()))
}
In this last step you'll learn how to test the Navigation component , using a different type of test double called a mock, and the testing library Mockito .
In this codelab you've used a test double called a fake. Fakes are one of many types of test doubles. Which test double should you use for testing the Navigation component ?
Think about how navigation happens. Imagine pressing one of the tasks in the TasksFragment
to navigate to a task detail screen.
Here's code in TasksFragment
that navigates to a task detail screen when it is pressed.
TasksFragment.kt
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
The navigation occurs because of a call to the navigate
method. If you needed to write an assert statement, there isn't a straightforward way to test whether you've navigated to TaskDetailFragment
. Navigating is a complicated action that doesn't result in a clear output or state change, beyond initializing TaskDetailFragment
.
What you can assert is that the navigate
method was called with the correct action parameter. This is exactly what a mock test double does—it checks whether specific methods were called.
Mockito is a framework for making test doubles. While the word mock is used in the API and name, it is not for just making mocks. It can also make stubs and spies.
You will be using Mockito to make a mock NavigationController
which can assert that the navigate method was called correctly.
Step 1. Add Gradle Dependencies
- Add the gradle dependencies.
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
—This is the Mockito dependency. -
dexmaker-mockito
—This library is required to use Mockito in an Android project. Mockito needs to generate classes at runtime. On Android, this is done using dex byte code, and so this library enables Mockito to generate objects during runtime on Android. -
androidx.test.espresso:espresso-contrib
—This library is made up of external contributions (hence the name) which contain testing code for more advanced views, such asDatePicker
andRecyclerView
. It also contains Accessibility checks and class calledCountingIdlingResource
that is covered later.
Step 2. Create TasksFragmentTest
- Open
TasksFragment
. - Right-click on the
TasksFragment
class name and select Generate then Test . Create a test in the androidTest source set. - Copy this code to the
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()
}
}
This code looks similar to the TaskDetailFragmentTest
code you wrote. It sets up and tears down a FakeAndroidTestRepository
. Add a navigation test to test that when you click on a task in the task list, it takes you to the correct TaskDetailFragment
.
- Add the test
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)
}
- Use Mockito's
mock
function to create a mock.
TasksFragmentTest.kt
val navController = mock(NavController::class.java)
To mock in Mockito, pass in the class you want to mock.
Next, you need to associate your NavController
with the fragment. onFragment
lets you call methods on the fragment itself.
- Make your new mock the fragment's
NavController
.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- Add the code to click on the item in the
RecyclerView
that has the text "TITLE1".
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))
RecyclerViewActions
is part of the espresso-contrib
library and lets you perform Espresso actions on a RecyclerView .
- Verify that
navigate
was called, with the correct argument.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
Mockito's verify
method is what makes this a mock—you're able to confirm the mocked navController
called a specific method ( navigate
) with a parameter ( actionTasksFragmentToTaskDetailFragment
with the ID of "id1").
The complete test looks like this:
@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")
)
}
- Run your test!
In summary, to test navigation you can:
- Use Mockito to create a
NavController
mock. - Attach that mocked
NavController
to the fragment. - Verify that navigate was called with the correct action and parameter(s).
Step 3. Optional, write clickAddTaskButton_navigateToAddEditFragment
To see if you can write a navigation test yourself, try this task.
- Write the test
clickAddTaskButton_navigateToAddEditFragment
which checks that if you click on the + FAB, you navigate to theAddEditTaskFragment
.
The answer is below.
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)
)
)
}
Click here to see a diff between the code you started and the final code.
To download the code for the finished codelab, you can use the git command below:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_2
یا میتوانید مخزن را بهعنوان یک فایل Zip دانلود کنید، آن را از حالت فشرده خارج کنید و در Android Studio باز کنید.
This codelab covered how to set up manual dependency injection, a service locator, and how to use fakes and mocks in your Android Kotlin apps. In particular:
- What you want to test and your testing strategy determine the kinds of test you are going to implement for your app. Unit tests are focused and fast. Integration tests verify interaction between parts of your program. End-to-end tests verify features, have the highest fidelity, are often instrumented, and may take longer to run.
- The architecture of your app influences how hard it is to test.
- TDD or Test Driven Development is a strategy where you write the tests first, then create the feature to pass the tests.
- To isolate parts of your app for testing, you can use test doubles. A test double is a version of a class crafted specifically for testing. For example, you fake getting data from a database or the internet.
- Use dependency injection to replace a real class with a testing class, for example, a repository or a networking layer.
- Use i nstrumented testing (
androidTest
) to launch UI components. - When you can't use constructor dependency injection, for example to launch a fragment, you can often use a service locator. The Service Locator pattern is an alternative to Dependency Injection. It involves creating a singleton class called the "Service Locator", whose purpose is to provide dependencies, both for the regular and test code.
دوره بی ادبی:
مستندات توسعه دهنده اندروید:
- Guide to app architecture
-
runBlocking
andrunBlockingTest
-
FragmentScenario
- Espresso
- Mockito
- JUnit4
- AndroidX Test Library
- AndroidX Architecture Components Core Test Library
- Source sets
- Test from the command line
Videos:
Other:
برای پیوند به دیگر کدلب ها در این دوره، صفحه فرود Advanced Android in Kotlin Codelabs را ببینید.