در این فصل میخواهیم با تست واحد یا Unit Testing در اندروید آشنا بشویم. برای این موضوع دو کتابخانه JUnit و Mockito را انتخاب میکنیم و با مباحث بسیار ساده و مقدماتی آنها آشنا میشویم و بعد به سراغ برنامه todo-mvp که پیش از این و در فصل قبل با آن آشنا شدید را انتخاب میکنیم و مباحث تست را بر روی این پروژه اجرا میکنیم.
Mockito چیست؟
ماکیتو یک کتابخانه بسیار کاربردی برای انجام بعضی کارها است که در تست واحد یا Unit Test به آنها احتیاج فراوان داریم. این کتابخانه کدباز (Open source) است و همانطور که از نامش پیدا است، کار اصلی آن ساختن اشیای Mock (یا مقلد) است. قطعاً میپرسید اشیای Mock چه اشیایی هستند؟ برای آشنایی با تکنیک Mocking در تست، تا انتهای این مطلب با ما باشید.
Mock چیست؟
برای تست کردن برنامه، باید بتوانیم همه جنبههای برنامه را تست کنیم. باید بتوانیم متدها را صدا بزنیم، آرگومانهای آنها را چک کنیم و خروجیها را بررسی کنیم تا ببینیم آیا همان خروجیهای مطلوب هستند یا نه. این کار همیشه به این سادگی امکانپذیر نیست. بعضی اشیا برای عملکرد درست نیاز به محیطی دارند که در آن اجرا شوند. مثلاً بسیاری از اشیای اندروید برای اجرا نیاز به Context دارند و Context برای این که عملکرد صحیحی داشته باشد شدیداً وابسته به محیط سیستم عامل اندروید است. تست چنین اشیایی به سادگی امکانپذیر نیست و نمیتوان آنها را در ماشین مجازی جاوا تست کرد. اگر تست ما وابسته به یکی از اشیا باشد، چطور باید برنامه را تست کنیم؟
به عنوان یک مثال دیگر درخواستهای وب را در نظر بگیرید. برای تست کردن عملکردی که نیاز به ارسال درخواستهای وب و تجزیه پاسخهای وب چه کاری باید کرد؟ اگر در زمان نوشتن تستها هنوز برنامه سرور آماده نباشد چه طور میشود برنامه را تست کرد و از عملکرد آن مطمئن شد؟
راهحل این مشکلات استفاده از تکنیک Mocking است. واژه Mock در زبان انگلیسی به معنای تقلید کردن است. یعنی به جای شی واقعی، یک شیای بسازیم که تقلیدی از آن است و عملکردی مطابق آنچه که مطلوب ما است دارد.
بگذارید با یک مثال توضیح بدهم. فرض کنید قرار است برنامهای بنویسیم که حقوق کارمندان را محاسبه میکند. برای این کار به دو کلاس احتیاج داریم:
- Employee: کلاسی که ویژگیهای کارمند در آن قرار دارد.
- EmployeeManager: کلاسی که مدیریت کارکنان را بر عهده دارد و بیشتر منطق کاری برنامه در آن قرار دارد.
اینترفیس EmployeeManager در این حالت شبیه قطعه کد زیر خواهد بود. متد getSalary حقوق دریافتی کارمند را محاسبه خواهد کرد:
public interface EmployeeManager { int getSalary(Employee emp); }
فرض کنید میخواهیم متدی برای محاسبه پاداش کارمندان بر اساس حقوق دریافتی آنها تعریف کنیم. پیادهسازی کلاس Employee شبیه قطعه کد زیر خواهد بود:
public class Employee { public int getBonus(EmployeeManager empManager) { int salary = empManager.getSalary(this) ; if (salary > 10000) { return (int) ((double)salary * .10d); } else { return (int) ((double)salary * .5d); } } }
فرض کنید در همین وضعیت میخواهیم متد getBonus کلاس Employee را تست کنیم تا ببینیم آیا به درستی عمل میکند یا نه. اما یک مسألهای هست: هنوز اینترفیس EmployeeManager را پیادهسازی نکردهایم. پس چطور میتوانیم متد getSalary را صدا زده و حقوق دریافتی کارمند را بدانیم تا بر اساس آن پاداش وی را محاسبه کنیم؟
اگر بخواهیم به شیوه معمول عمل کنیم، باید کلاسی بسازیم و یک پیادهسازی برای این متد در آن بنویسیم:
public class FakeEmployeeManager { public int getSalary(Employee emp) { return 10000; } }
اما این روش هم مشکلات زیادی دارد. اگر بخواهیم این تابع را برای مقادیر مختلف تست کنیم نیاز به چندین پیادهسازی مختلف داریم؛ اگر متدهای این کلاس بیشتر باشد باید برای همه آنها یک پیادهسازی الکی درست کنیم تا برنامه کامپایل شود در حالی که ما فقط متد getSalary را میخواهیم تست کنیم و مشکلات دیگر…
اینجا تکنیک Mock به کمک ما میآید. به کمک این تکنیک ما نمونهای از این کلاس میسازیم که هیچ پیادهسازیای برای متدهایش ندارد و ما در تستهای خودمان تعریف میکنیم که اگر هر متدی صدا زده شد چه رفتاری از خودش بروز دهد.
برای ساختن Mock دو روش وجود دارد: روش اول استفاده از متد mock است:
EmployeeManager empManager = mock(EmployeeManager.class);
روش دوم استفاده از حاشیهنوشت @Mock است:
@Mock EmployeeManager empManager;
توجه: اگر از این شیوه استفاده میکنید، حتماً باید قبل از اجرای تست، متد استاتیک initMocks از کلاس MockitoAnnotations را صدا بزنید:
MockitoAnnotations.initMocks(this);
حالا یک شی مقلد یا Mock بر اساس کلاس EmployeeManager داریم و میتوانیم از آن در تست کلاس Employee استفاده کنیم:
Employee sam = new Employee(); when(empManager.getSalary(sam)).thenReturn(12000);
اینجا از شیء مقلد یا Mock ساخته شده از اینترفیس EmployeeManager میخواهیم که هر وقت متد getSalary برای شیای به نام sam که از کلاس Employee صدا زده شد، مقدار ۱۲۰۰۰ را برگرداند. به همین سادگی!
حالا اگر بخواهیم برای یک شیء دیگر همین متد را صدا زده و مقدار متفاوتی بگیریم کافی است این طوری عمل کنیم:
Employee sam = new Employee(); when(empManager.getSalary(sam)).thenReturn(12000); Employee john = new Employee(); when(empManager.getSalary(john)).thenReturn(8000);
متدهای when و then را متدهای stubbing مینامند. کار این متدها به زبان ساده این است:
اگر متد getSalary بر روی شیء Mock یا مقلد empManager صدا زده شده و پارامتر ورودی آن شیای از نوع Employee با نام sam است، مقدار ۱۲۰۰۰ را برگردان.
اگر متد getSalary بر روی شیء Mock یا مقلد empManager صدا زده شده و پارامتر ورودی آن شیای از نوع Employee با نام john است، مقدار ۸۰۰۰ را برگردان.
حالا میتوانیم بدون این که یک نمونه واقعی از کلاس EmployeeManager را پیادهسازی کنیم، متد getBonus کلاس Employee را تست کنیم:
Assert.assertEquals(1200, sam.getBonus(empManager)); Assert.assertEquals(4000, john.getBonus(empManager));
کلاس Assert یکی از کلاسهای اصلی کتابخانه JUnit است. معنای کلمه assert در انگلیسی «اثبات کردن» است. این دو خط قسمت اصلی تست ما است. ما میخواهیم ادعای خودمان را اثبات کنیم. ادعای ما چیست؟ ما مدعی هستیم که اگر متد getBonus شیء sam را صدا بزنیم، مقداری که باید بگیریم ۱۲۰۰ است. اگر مقدار دریافتی با مقدار مورد نظر ما برابر بود، به معنی این است که متد getBonus کلاس Employee کار خود را به درستی انجام داده است و درستی ادعای ما ثابت میشود و تست با موفقیت به اتمام میرسد. اما اگر مقدار دریافتی از تابع با مقدار مورد ادعای ما برابر نبود، به معنی این است که متد getBonus کار خود را به هر دلیلی به درستی انجام نداده است و تست ناموفق بوده است.
مرحله آخر این است که مطمئن شویم متد getSalary شیء مقلد empManager با آرگومانهای ورودی sam و john صدا زده شده است:
verify(empManager).getSalary(sam); verify(empManager).getSalary(john);
متن کامل کلاس تست ما که نام آن را EmployeeTest گذاشتهایم شبیه قطعه کد زیر است:
import static org.mockito.Mockito.*; import org.testng.Assert; import org.testng.annotations.Test; import com.javarticles.mokcito.Employee; import com.javarticles.mokcito.EmployeeManager; public class EmployeeTest { @Test public void stubEmpBehavior() { EmployeeManager empManager = mock(EmployeeManager.class); Employee sam = new Employee(); when(empManager.getSalary(sam)).thenReturn(12000); Employee john = new Employee(); when(empManager.getSalary(john)).thenReturn(8000); Assert.assertEquals(1200, sam.getBonus(empManager)); Assert.assertEquals(4000, john.getBonus(empManager)); verify(empManager).getSalary(sam); verify(empManager).getSalary(john); } }
حالا بیایید یک متد جدید به EmployeeManager اضافه کنیم و عملکرد آن را تست کنیم. این متد join نام دارد و کار آن ثبت یک Employee جدید است:
public interface EmployeeManager { int getSalary(Employee emp); void join(Employee emp); }
حالا میخواهیم یک تست برای این متد بنویسیم:
@Test public void employeeJoins() { Employee sam = new Employee(); EmployeeManager empManager = mock(EmployeeManager.class); empManager.join(sam); verify(empManager).join(sam); }
حالا دیگر میدانید که این تست چه کار میکند.
کنترل ترتیب فراخوانیها با InOrder
یکی دیگر از امکاناتی که Mockito در اختیار ما میگذارد، بررسی ترتیب فراخوانی متدها است. مثلاً در زمانی که میخواهیم اطلاعاتی را از اینترنت بگیریم، برای ما مهم است که ابتدا Loading نمایش داده شود، بعد اطلاعات گرفته شود و در انتها Loading حذف شود. برای این کار از کلاس InOrder کتابخانه ماکیتو استفاده میکنیم:
InOrder inOrder = inOrder(mTaskDetailView); inOrder.verify(mTaskDetailView).setLoadingIndicator(true); mGetTaskCallbackCaptor.getValue().onTaskLoaded(ACTIVE_TASK); // Trigger callback inOrder.verify(mTaskDetailView).setLoadingIndicator(false);
حالا اگر ابتدا متد setLoadingIndicator با مقدار false فراخوانی شود (به معنای نشان ندادن Loading)، تست ناموفق خواهد بود و ما میدانیم که جایی در ترتیب فراخوانی متدها اشتباهی رخ داده است.
بررسی آرگومانها با استفاده از کلاس ArgumentCaptor
مواقعی پیش میآید که میخواهیم آرگومانهای رسیده به متد و نوع و مقدار آنها را تست کنیم. برای این کار از کلاس ArgumentCaptor یا از حاشیهنوشته @Captor استفاده میکنیم:
ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class); verify(mock).doSomething(argument.capture()); assertEquals("John", argument.getValue().getName());
با
@Captor private ArgumentCaptor<Person> argument; // ... verify(mock).doSomething(argument.capture()); assertEquals("John", argument.getValue().getName());
در ادامه مطلب بیشتر با ابزارها و امکانات Mockito آشنا خواهیم شد.
تست برنامه todo-mvp
با برنامه todo-mvp در این مطلب سایت آشنا شدیم. یکی از مزایای مطالعه این برنامه نمونه گوگل این است که این برنامه همه انواع تست را هم دارد و ما میتوانیم به عنوان الگویی برای تست برنامه، به این مثال مراجعه کنیم. در این بخش میخواهیم مروری داشته باشیم به تست کلاس TaskDetailPresenterTest در این برنامه نمونه.
اگر به کدهای این برنامه نگاهی بیاندازید میبینید که چند شاخه برای تستهای مختلف دارد:
اگر آخرین شاخه را باز کنید، میبینید که به ازای هر بخش برنامه یک شاخه دارد و در هر کدام آنها برای کلاسهای معرف یا Presenter مرتبط، یک کلاس تست نوشته شده است:
حالا کلاس TaskDetailPresenterTest را باز میکنیم تا ببینیم چطور باید برای این معرف تست بنویسیم تا از صحت عملکرد آن مطمئن بشویم:
package com.example.android.architecture.blueprints.todoapp.taskdetail; import com.example.android.architecture.blueprints.todoapp.data.Task; import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource; import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * Unit tests for the implementation of {@link TaskDetailPresenter} */ public class TaskDetailPresenterTest { public static final String TITLE_TEST = "title"; public static final String DESCRIPTION_TEST = "description"; public static final String INVALID_TASK_ID = ""; public static final Task ACTIVE_TASK = new Task(TITLE_TEST, DESCRIPTION_TEST); public static final Task COMPLETED_TASK = new Task(TITLE_TEST, DESCRIPTION_TEST, true); @Mock private TasksRepository mTasksRepository; @Mock private TaskDetailContract.View mTaskDetailView; /** * {@link ArgumentCaptor} is a powerful Mockito API to capture argument values and use them to * perform further actions or assertions on them. */ @Captor private ArgumentCaptor<TasksDataSource.GetTaskCallback> mGetTaskCallbackCaptor; private TaskDetailPresenter mTaskDetailPresenter; @Before public void setup() { // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To // inject the mocks in the test the initMocks method needs to be called. MockitoAnnotations.initMocks(this); // The presenter won't update the view unless it's active. when(mTaskDetailView.isActive()).thenReturn(true); } @Test public void getActiveTaskFromRepositoryAndLoadIntoView() { // When tasks presenter is asked to open a task mTaskDetailPresenter = new TaskDetailPresenter( ACTIVE_TASK.getId(), mTasksRepository, mTaskDetailView); mTaskDetailPresenter.start(); // Then task is loaded from model, callback is captured and progress indicator is shown verify(mTasksRepository).getTask(eq(ACTIVE_TASK.getId()), mGetTaskCallbackCaptor.capture()); InOrder inOrder = inOrder(mTaskDetailView); inOrder.verify(mTaskDetailView).setLoadingIndicator(true); // When task is finally loaded mGetTaskCallbackCaptor.getValue().onTaskLoaded(ACTIVE_TASK); // Trigger callback // Then progress indicator is hidden and title, description and completion status are shown // in UI inOrder.verify(mTaskDetailView).setLoadingIndicator(false); verify(mTaskDetailView).showTitle(TITLE_TEST); verify(mTaskDetailView).showDescription(DESCRIPTION_TEST); verify(mTaskDetailView).showCompletionStatus(false); } @Test public void getCompletedTaskFromRepositoryAndLoadIntoView() { mTaskDetailPresenter = new TaskDetailPresenter( COMPLETED_TASK.getId(), mTasksRepository, mTaskDetailView); mTaskDetailPresenter.start(); // Then task is loaded from model, callback is captured and progress indicator is shown verify(mTasksRepository).getTask( eq(COMPLETED_TASK.getId()), mGetTaskCallbackCaptor.capture()); InOrder inOrder = inOrder(mTaskDetailView); inOrder.verify(mTaskDetailView).setLoadingIndicator(true); // When task is finally loaded mGetTaskCallbackCaptor.getValue().onTaskLoaded(COMPLETED_TASK); // Trigger callback // Then progress indicator is hidden and title, description and completion status are shown // in UI inOrder.verify(mTaskDetailView).setLoadingIndicator(false); verify(mTaskDetailView).showTitle(TITLE_TEST); verify(mTaskDetailView).showDescription(DESCRIPTION_TEST); verify(mTaskDetailView).showCompletionStatus(true); } @Test public void getUnknownTaskFromRepositoryAndLoadIntoView() { // When loading of a task is requested with an invalid task ID. mTaskDetailPresenter = new TaskDetailPresenter( INVALID_TASK_ID, mTasksRepository, mTaskDetailView); mTaskDetailPresenter.start(); verify(mTaskDetailView).showMissingTask(); } @Test public void deleteTask() { // Given an initialized TaskDetailPresenter with stubbed task Task task = new Task(TITLE_TEST, DESCRIPTION_TEST); // When the deletion of a task is requested mTaskDetailPresenter = new TaskDetailPresenter( task.getId(), mTasksRepository, mTaskDetailView); mTaskDetailPresenter.deleteTask(); // Then the repository and the view are notified verify(mTasksRepository).deleteTask(task.getId()); verify(mTaskDetailView).showTaskDeleted(); } @Test public void completeTask() { // Given an initialized presenter with an active task Task task = new Task(TITLE_TEST, DESCRIPTION_TEST); mTaskDetailPresenter = new TaskDetailPresenter( task.getId(), mTasksRepository, mTaskDetailView); mTaskDetailPresenter.start(); // When the presenter is asked to complete the task mTaskDetailPresenter.completeTask(); // Then a request is sent to the task repository and the UI is updated verify(mTasksRepository).completeTask(task.getId()); verify(mTaskDetailView).showTaskMarkedComplete(); } @Test public void activateTask() { // Given an initialized presenter with a completed task Task task = new Task(TITLE_TEST, DESCRIPTION_TEST, true); mTaskDetailPresenter = new TaskDetailPresenter( task.getId(), mTasksRepository, mTaskDetailView); mTaskDetailPresenter.start(); // When the presenter is asked to activate the task mTaskDetailPresenter.activateTask(); // Then a request is sent to the task repository and the UI is updated verify(mTasksRepository).activateTask(task.getId()); verify(mTaskDetailView).showTaskMarkedActive(); } @Test public void activeTaskIsShownWhenEditing() { // When the edit of an ACTIVE_TASK is requested mTaskDetailPresenter = new TaskDetailPresenter( ACTIVE_TASK.getId(), mTasksRepository, mTaskDetailView); mTaskDetailPresenter.editTask(); // Then the view is notified verify(mTaskDetailView).showEditTask(ACTIVE_TASK.getId()); } @Test public void invalidTaskIsNotShownWhenEditing() { // When the edit of an invalid task id is requested mTaskDetailPresenter = new TaskDetailPresenter( INVALID_TASK_ID, mTasksRepository, mTaskDetailView); mTaskDetailPresenter.editTask(); // Then the edit mode is never started verify(mTaskDetailView, never()).showEditTask(INVALID_TASK_ID); // instead, the error is shown. verify(mTaskDetailView).showMissingTask(); } }
بررسی کلاس و توضیحات:
۱- اولین موضوع در بررسی این کلاس، تعریف اشیای Mock یا مقلد در خطوط ۵۲ تا ۵۶ است:
@Mock private TasksRepository mTasksRepository; @Mock private TaskDetailContract.View mTaskDetailView;
چرا این دو کلاس را Mock میکنیم؟ چون هر دوی آنها برای اجرا به محیط اندروید احتیاج دارند ولی ما کلاس معرف را تست میکنیم نه پیادهسازی این کلاسها را و مهم این است که ببینیم آیا Presenter کار خود را به درستی انجام میدهد یا نه.
۲- دومین نکتهای که نیاز به توضیح دارد استفاده از @Captor در خطوط ۶۲ و ۶۳ است:
@Captor private ArgumentCaptor<TasksDataSource.GetTaskCallback> mGetTaskCallbackCaptor;
با استفاده از این متغیر میتوانیم تأیید کنیم که آیا در فراخوانی متد getTask آرگومانی از نوع TasksDataSource.GetTaskCallback به این تابع ارسال شده است یا نه؟
۳- نکته بعدی حاشیهنوشت @Before است. در بسیاری مواقع برای اجرای تستها نیاز به فراهم کردن برخی مقدمات است. تابعی که با حاشیهنوشت @Before مشخص میشود، پیش از همه تستها صدا زده شده و مقداردهی اولیه را انجام میدهد:
@Before public void setup() { MockitoAnnotations.initMocks(this); when(mTaskDetailView.isActive()).thenReturn(true); }
همانطور که میبینید در این تابع ابتدا با استفاده از متد initMocks اشیای مقلد یا Mock را میسازیم و در خط ۷۴ متد isActive شیء مقلد mTaskDetailView را اصطلاحاً Stub میکنیم تا همیشه موقع فراخوانی این متد مقدار true را برگرداند. اگر این متد false برگرداند به معنی این است که هنوز نمایشگر آماده نیست و معرف دیگر متدهای نمایشگر را صدا نمیزند.
۴- با استفاده از حاشیهنوشت @Test قبل از متدهای تست، به JUnit اعلام میکنیم که این متد، یک متد تست است. در این کلاس ۸ متد تست وجود دارد.
۵- حالا متد تست getActiveTaskFromRepositoryAndLoadIntoView را بررسی میکنیم. اولین نکته این است که نام این متد باید تا حد امکان گویای کاری باشد که این متد قرار است تست کند. همانطور که احتمالاً از نام این متد فهمیدهاید، این متد فرایند گرفتن Task های Active از Repository و بارگذاری آنها در نمایشگر را تست میکند:
@Test public void getActiveTaskFromRepositoryAndLoadIntoView() { // When tasks presenter is asked to open a task mTaskDetailPresenter = new TaskDetailPresenter( ACTIVE_TASK.getId(), mTasksRepository, mTaskDetailView); mTaskDetailPresenter.start(); // Then task is loaded from model, callback is captured and progress indicator is shown verify(mTasksRepository).getTask(eq(ACTIVE_TASK.getId()), mGetTaskCallbackCaptor.capture()); InOrder inOrder = inOrder(mTaskDetailView); inOrder.verify(mTaskDetailView).setLoadingIndicator(true); // When task is finally loaded mGetTaskCallbackCaptor.getValue().onTaskLoaded(ACTIVE_TASK); // Trigger callback // Then progress indicator is hidden and title, description and completion status are shown // in UI inOrder.verify(mTaskDetailView).setLoadingIndicator(false); verify(mTaskDetailView).showTitle(TITLE_TEST); verify(mTaskDetailView).showDescription(DESCRIPTION_TEST); verify(mTaskDetailView).showCompletionStatus(false); }
حالا میخواهیم ببینیم این تست چه کاری انجام میدهد و چه چیزی را تست میکند و چگونه.
۱- اولین کاری که میکنیم یه نمونه یا شیء از روی کلاس معرف TaskDetailPresenter به نام mTaskDetailPresenter میسازیم و مقداردهی میکنیم. همانطور که میبینید، شناسه یا Id یک Task که قبلاً تعریف شده است و دو شی Mock یا مقلد mTasksRepository و mTaskDetailView را به این شیء میدهیم.
۲- حالا متد start کلاس معرف را صدا میزنیم. اگر به پیادهسازی اصلی این متد در کلاس TaskDetailPresenter نگاهی بیاندازید میبینید که کار اصلی آن گرفتن Task از دیتابیس یا اینترنت و بروزرسانی UI نمایشگر است. ما برای این که مطمئن شویم این متد به درستی کار خودش را انجام میدهد، باید ببینیم که آیا متدهای مربوطه در mTaskDetailView و mTasksRepository را صدا میزند یا نه؟ و آیا آنها را به ترتیب منطقی صدا میزند؟ آرگومانها به درستی ارسال میشوند و مانند اینها.
۳- بعد از صدا زدن متد start حالا بررسی میکنیم تا ببینیم آیا همه چیز به خوبی پیش رفته است یا نه. در واقع تست اصلی از اینجا شروع میشود:
verify(mTasksRepository).getTask(eq(ACTIVE_TASK.getId()), mGetTaskCallbackCaptor.capture());
۳-۱- اول از همه باید ببینیم آیا متد getTask کلاس TasksRepository با دو آرگومان شناسه Task و شنوندهای (listener) از نوع TasksDataSource.GetTaskCallback صدا زده شده است یا نه؟ و اگر صدا زده شده است آیا مقدار شناسه Task برابر با همان مقداری که ما انتظار داریم یعنی ACTIVE_TASK.getId() است یا نه؟ اگر همه چیز به درستی پیش رفته باشد، از این تست با موفقیت میگذریم.
۳-۲- حالا میخواهیم ببینیم آیا متدهای نمایشگر ما به درستی و بر اساس ترتیب منطقی صدا زده شدهاند؟ برای این کار یک شیء InOrder تعریف میکنیم:
InOrder inOrder = inOrder(mTaskDetailView);
۳-۳- حالا میخواهیم ببینیم که آیا بعد از صدا زدن متد getTask آیا متد setLoadingIndicator نمایشگر با آرگومان true صدا زده شده است؟ کار این متد نشان دادن Loading است.
۳-۴- حالا میخواهیم تست کنیم و ببینیم آیا پس از گرفتن Task از TasksRepository آیا متدهای Callback مربوطه به درستی صدا زده میشوند و مقادیر دریافتی آنها درست است؟ برای این کار متد onTaskLoaded کلاس TasksDataSource.GetTaskCallback را صدا میزنیم و ACTIVE_TASK را به عنوان پارامتر به این متد ارسال میکنیم:
mGetTaskCallbackCaptor.getValue().onTaskLoaded(ACTIVE_TASK);
۳-۵- بعد از فعال کردن و صدا زدن این متد انتظار داریم نمایش Loading متوقف شود و متدهای showTitle با پارامتر TITLE_TEST و showDescription با پارامتر DESCRIPTION_TEST و متد showCompletionStatus با پارامتر false صدا زده شده باشند. همانطور که میدانید برای تست این موضوع از متد verify استفاده میکنیم:
inOrder.verify(mTaskDetailView).setLoadingIndicator(false); verify(mTaskDetailView).showTitle(TITLE_TEST); verify(mTaskDetailView).showDescription(DESCRIPTION_TEST); verify(mTaskDetailView).showCompletionStatus(false);
اجرای تست
برای اجرای یک تست خاص به سادگی میتوانید دکمه مثلث سبز رنگ کنار تست را بزنیم:
نتیجه اجرای این تست را ببینید:
اگر بخواهیم همه تستهای یک کلاس را اجرا کنیم، بر روی کلاس راست کلیک میکنیم و گزینه Run را میزنیم:
بررسی خطای تست
اگر در اجرای یک تست خطایی رخ بدهد و تست اصطلاحاً fail بشود چه اتفاقی میافتد؟ بیایید در تست خودمان کمی دستکاری کنیم. برای این کار به سراغ کلاس TaskDetailPresenter میرویم و کمی آن را تغییر میدهیم. اگر کد متد openTask را ببینید متوجه میشوید که پس از گرفتن موفق Task از TasksRepository تابع showTask صدا زده میشود تا نمایشگر را بروزرسانی کند. اگر کمی در این متد دستکاری کنیم، نتیجه تست ما fail خواهد شد. برای این کار ما فقط خط اول این متد را تغییر میدهیم و کلمه failed را به ابتدای عنوان Task اضافه میکنیم:
private void showTask(@NonNull Task task) { String title = "failed " + task.getTitle(); // ... }
حالا ببینیم همین تغییر ساده چطور باعث میشود تست fail شود:
به همین سادگی میفهمیم که چیزی آن طور که باید کار نمیکند! به کمک سیستمهای کنترل نسخه کد (Version Control System) مانند Git به سادگی میتوان تشخیص داد که این کد را آخرین بار چه کسی تغییر داده است و بدون این که تست کند، منتشر کرده است!
تست واحد (Unit Testing) مبحث بسیار وسیعی است و این مطلب در حد یک مقدمه برای JUint و Mockito است. توصیه میکنم حتماً در این زمینه تخصص خودتان را بالا ببرید. یکی از عناوین شغلی بسیار مهم در دنیای نرمافزار تخصص در حوزه تست نرمافزار است و در ایران هم به تازگی بسیار مورد توجه قرار گرفته است.













