آموزش اندروید-فصل ۲۸-۳: تست برنامه‌های MVP

ما ویژگی‌های الگوی معماری MVP را در بخش اول مطلب آموزش  دادیم و در بخش دوم هم این الگو را در برنامه خودمان پیاده‌سازی کردیم. حالا وقت آن است که کمی بیشتر به اعماق برویم. در این آموزش بر روی موضوعات زیر تمرکز می‌کنیم:

  • آماده‌سازی محیط تست برنامه و نوشتن تست واحد (Unit Test) برای کلاس‌های MVP
  • پیاده‌سازی MVP‌ به همراه تزریق وابستگی با استفاده از Dagger2
  • و در اخر درباره مشکلات متداول در اندروید که باید از آن‌ها اجتناب کرد صحبت خواهیم کرد.

۱- تست واحد (Unit Test)

یکی از بزرگ‌ترین نتایج استفاده از الگوی MVP ساده شدن فرایند تست برنامه است. پس بیایید برای کلاس‌های مدل و معرفی که در قسمت قبل ساختیم تست بنویسیم. تست‌ها را با استفاده از Robolectric اجرا می‌کنیم که یک چهارچوب تست واحد است که امکانات خیلی خوبی برای کلاس‌های اندروید فراهم آورده است. برای ساخت اشیای قالب (Mock Objects) از Mockito استفاده خواهیم کرد که به ما اجازه می‌دهد بررسی کنیم که آیا کتدهای خاصی صدا زده شده‌اند یا نه.

گام اول: آماده‌سازی

فایل build.gradle مربوط به ماژول app را ویرایش کنید و وابستگی‌های زیر را به آن اضافه کنید:

dependencies {
    //…
    testCompile 'junit:junit:4.12'
    // Set this dependency if you want to use Hamcrest matching
    testCompile 'org.hamcrest:hamcrest-library:1.1'
    testCompile "org.robolectric:robolectric:3.0"
    testCompile 'org.mockito:mockito-core:1.10.19'
}

در شاخه src پروژه شاخه‌ای با ساختار زیر بسازید: test/java/[package-name]/[app-name] بعد از آن تنظیمات مربوط به تست را به برنامه اضافه کنید. برای این کار دکمه Edit configuration را کلیک کنید: ch-28-10-Unit-Test-setup بر روی دکمه + کلیک کنید و از لیست JUnit را انتخاب کنید: ch-28-11-Unit-Test-setup مقدار Working Directory را $MODULE_DIR$ بگذارید: ch-28-12-Unit-Test-setupاین تنظیمات برای اجرای همه تست‌ها است بنابراین در قسمت Test Kind مقدار All in package را انتخاب کنید و بعد نام بسته یا پکیج برنامه را در قسمت Package بنویسید. ch-28-13-Unit-Test-setup

گام ۲: تست کردن مدل

بیایید تست خود را با کلاس مدل شروع کنیم. تست را کلاس RobolectricGradleTestRunner اجرا خواهد کرد که لوازم و منابع مورد نیاز برای عملیات ویژه اندروید را فراهم کرده است. ضروری است که حاشیه‌نوشت @Config را با مقادیر زیر تنظیم کنیم:

@RunWith(RobolectricGradleTestRunner.class)
// Change what is necessary for your project
@Config(constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml")
public class MainModelTest {
    // write the tests
}

می‌خواهیم که از یک DAO واقعی استفاده کنیم تا ببینیم آیا داده‌ها به درستی مدیریت می‌شوند یا نه. برای دسترسی به شی Context از کلاس RuntimeEnvironment.application استفاده می‌کنیم.

private DAO mDAO;
// To test the Model you can just
// create the object and and pass
// a Presenter mock and a DAO instance
@Before
public void setup() {
    // Using RuntimeEnvironment.application will permit
    // us to access a Context and create a real DAO
    // inserting data that will be saved temporarily
    Context context = RuntimeEnvironment.application;
    mDAO = new DAO(context);
    // Using a mock Presenter will permit to verify
    // if certain methods were called in Presenter
    MainPresenter mockPresenter = Mockito.mock(MainPresenter.class);
    // We create a Model instance using a construction that includes
    // a DAO. This constructor exists to facilitate tests
    mModel = new MainModel(mockPresenter, mDAO);
    // Subscribing mNotes is necessary for tests  methods
    // that depends on the arrayList
    mModel.mNotes = new ArrayList<>();
    // We’re reseting our mock Presenter to guarantee that
    // our method verification remain consistent between the tests
    reset(mockPresenter);
}

حالا وقت آن است که متدهای کلاس Model را تست کنیم.

// Create Note object to use in the tests
private Note createNote(String text) {
    Note note = new Note();
    note.setText(text);
    note.setDate("some date");
    return note;
}
// Verify loadData
@Test
public void loadData(){
    int notesSize = 10;
    // inserting data directly using DAO
    for (int i =0; i<notesSize; i++){
        mDAO.insertNote(createNote("note_" + Integer.toString(i)));
    }
    // calling load method
    mModel.loadData();
    // verify if mNotes, an ArrayList that receives the Notes
    // have the same size as the quantity of Notes inserted
    assertEquals(mModel.mNotes.size(), notesSize);
}

// verify insertNote
@Test
public void insertNote() {
    int pos = mModel.insertNote(createNote("noteText"));
    assertTrue(pos > -1);
}

// Verify deleteNote
@Test
public void deleteNote() {
    // We need to add a Note in DB
    Note note = createNote("testNote");
    Note insertedNote = mDAO.insertNote(note);
    // add the same Note inside mNotes ArrayList
    mModel.mNotes = new ArrayList<>();
    mModel.mNotes.add(insertedNote);
    // verify if deleteNote returns the correct results
    assertTrue(mModel.deleteNote(insertedNote, 0));
    Note fakeNote = createNote("fakeNote");
    assertFalse(mModel.deleteNote(fakeNote, 0));
}

حالا می‌توانیم تست کلاس Model را اجرا و نتایج آن را بررسی کنیم. بقیه ویژگی‌های کلاس را هم با خیال راحت تست کنید.

گام ۳: تست معرف (Presenter)

حالا بیایید بر روی تست معرف تمرکز کنیم. برای این تست به Robolectric احتیاج داریم تا بتوانیم از بعضی کلاس‌های Android مانند AsyncTask استفاده کنیم. تنظیمات این تست بسیار شبیه تست کلاس Model است. ما از نمایشگر و مدل قالبی (Mock) برای تأیید صدا زدن متدها و مقادیر برگشتی استفاده می‌کنیم.

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, manifest = "/src/main/AndroidManifest.xml")
public class MainPresenterTest {
    private MainPresenter mPresenter;
    private MainModel mockModel;
    private MVP_Main.RequiredViewOps mockView;
    // To test the Presenter you can just
    // create the object and pass the Model and View mocks
    @Before
    public void setup() {
        // Creating the mocks
        mockView = Mockito.mock( MVP_Main.RequiredViewOps.class );
        mockModel = Mockito.mock( MainModel.class, RETURNS_DEEP_STUBS );
        // Pass the mocks to a Presenter instance
        mPresenter = new MainPresenter( mockView );
        mPresenter.setModel(mockModel);
        // Define the value to be returned by Model
        // when loading data
        when(mockModel.loadData()).thenReturn(true);
        reset(mockView);
    }
}

برای تست متدهای کلاس معرف بیایید از عملیات clickNewNote() شروع کنیم که مسئولیت ایجاد یک یادداشت جدید و ثبت آن در دیتابیس با استفاده از AsyncTask بر عهده دارد.

@Test
public void testClickNewNote() {
    // We need to mock a EditText
    EditText mockEditText = Mockito.mock(EditText.class, RETURNS_DEEP_STUBS);
    // the mock should return a String
    when(mockEditText.getText().toString()).thenReturn(“Test_true");
    // we also define a fake position to be returned
    // by the insertNote method in Model
    int arrayPos = 10;
    when(mockModel.insertNote(any(Note.class))).thenReturn(arrayPos);

    mPresenter.clickNewNote(mockEditText);
    verify(mockModel).insertNote(any(Note.class));
    verify(mockView).notifyItemInserted( eq(arrayPos+1) );
    verify(mockView).notifyItemRangeChanged(eq(arrayPos), anyInt());
    verify(mockView, never()).showToast(any(Toast.class));
}

ما می‌توانیم یک سناریوی تست طراحی کنیم که در آن متد insertNote() خطا برمی‌گرداند.

@Test
public void testClickNewNoteError() {
    EditText mockEditText = Mockito.mock(EditText.class, RETURNS_DEEP_STUBS);
    when(mockModel.insertNote(any(Note.class))).thenReturn(-1);
    when(mockEditText.getText().toString()).thenReturn("Test_false");
    when(mockModel.insertNote(any(Note.class))).thenReturn(-1);
    mPresenter.clickNewNote(mockEditText);
    verify(mockView).showToast(any(Toast.class));
}

در آخر هم متد deleteNote() را در هر دو حالت اجرای درست و با مقدار برگشتی خطا تست می‌کنیم.

@Test
public void testDeleteNote(){
    when(mockModel.deleteNote(any(Note.class), anyInt())).thenReturn(true);
    int adapterPos = 0;
    int layoutPos = 1;
    mPresenter.deleteNote(new Note(), adapterPos, layoutPos);
    verify(mockView).showProgress();
    verify(mockModel).deleteNote(any(Note.class), eq(adapterPos));
    verify(mockView).hideProgress();
    verify(mockView).notifyItemRemoved(eq(layoutPos));
    verify(mockView).showToast(any(Toast.class));
}

@Test
public void testDeleteNoteError(){
    when(mockModel.deleteNote(any(Note.class), anyInt())).thenReturn(false);
    int adapterPos = 0;
    int layoutPos = 1;
    mPresenter.deleteNote(new Note(), adapterPos, layoutPos);
    verify(mockView).showProgress();
    verify(mockModel).deleteNote(any(Note.class), eq(adapterPos));
    verify(mockView).hideProgress();
    verify(mockView).showToast(any(Toast.class));
}

۲- تزریق وابستگی با استفاده از Dagger 2

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

تزریق وابستگی روشی برای تنظیم مقادیر یک شی و همکاران آن توسط یک نهاد خارجی است. به بیان دیگر اشیا را یک نهاد بیرونی تنظیم می‌کند. تزریق وابستگی روش جایگزین این است که یک شی خودش خودش را تنظیم کند. یاکوب ینکوف

در مثال ما، تزریق وابستگی امکان می‌دهد که مدل و معرف بیرون از نمایشگر ایجاد شوند و این باعث می‌شود که لایه‌های معماری MVP کمتر به هم وابستگی داشته باشند و با این کار جدایی موضوعات بیشتری در پروژه اعمال می‌شود. ما از Dagger 2 استفاده می‌کنیم، کتابخانه‌ای فوق‌العاده از طرف گوگل، تا در تزریق وابستگی به ما کمک کند. با این که آماده‌سازی و تنظیم Dagger 2 بسیار ساده  سر راست است، ولی این کتابخانه امکانات عالی فراوانی دارد که آن را کتابخانه‌ سودمند ولی پیچیده‌ای می‌کند. ما فقط بر روی قسمت‌هایی از این کتابخانه که برای پیاده‌سازی MVP به آن‌ها احتیاج داریم می‌پردازیم و خیلی وارد جزئیات این کتابخانه نمی‌شویم. اگر می‌خواهید بیشتر درباره Dagger بدانید، مقاله «کری» را بخوانید یا به مستنداتی که گوگل فراهم آورده است سری بزنید.

گام ۱: تنظیم Dagger 2

با تغییر در فایل build.gradle پروژه و افزودن وابستگی زیر آغاز کنید:

dependencies {
    // ...
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}

سپس فایل build.dagger پروژه را مانند نمونه زیر تغییر بدهید.

apply plugin: 'com.neenbedankt.android-apt'

dependencies {
    // apt command comes from the android-apt plugin
    apt 'com.google.dagger:dagger-compiler:2.0.2'
    compile 'com.google.dagger:dagger:2.0.2'
    provided 'org.glassfish:javax.annotation:10.0-b28'
    // ...
}

پروژه را همکام یا Sync کنید و تا انتهای این کار صبر کنید.

گام ۲: پیاده‌سازی MVP با Dagger 2

بیایید با ساخت @Scope برای کلاس‌های اکتیویتی شروع کنیم. ActivityScope را همانند کد زیر بسازید:

@Scope
public  @interface ActivityScope {
}

حالا باید MainActivity را به عنوان یک @Module معرفی کنیم. اگر بیشتر از یک اکتیویتی داشته باشید، باید برای همه آن‌ها این کار را بکنید.

@Module
public class MainActivityModule {

    private MainActivity activity;

    public MainActivityModule(MainActivity activity) {
        this.activity = activity;
    }

    @Provides
    @ActivityScope
    MainActivity providesMainActivity() {
        return activity;
    }

    @Provides
    @ActivityScope
    MVP_Main.ProvidedPresenterOps providedPresenterOps() {
        MainPresenter presenter = new MainPresenter( activity );
        MainModel model = new MainModel( presenter );
        presenter.setModel( model );
        return presenter;
    }

}

ما همچنین به یک @Subcomponent نیاز داریم که پلی باشد برای @Component برنامه که باید آن را هم بسازیم.

@ActivityScope
@Subcomponent( modules = MainActivityModule.class )
public interface MainActivityComponent {
    MainActivity inject(MainActivity activity);
}

باید یک @Module و یک @Component برای Application بسازیم.

@Module
public class AppModule {

    private Application application;

    public AppModule(Application application) {
        this.application = application;
    }

    @Provides
    @Singleton
    public Application providesApplication() {
        return application;
    }
}
@Singleton
@Component( modules = AppModule.class)
public interface AppComponent {
    Application application();
    MainActivityComponent getMainComponent(MainActivityModule module);
}

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

public class SampleApp extends Application {

    public static SampleApp get(Context context) {
        return (SampleApp) context.getApplicationContext();
    }

    @Override
    public void onCreate() {
        super.onCreate();

        initAppComponent();
    }

    private AppComponent appComponent;

    private void initAppComponent(){
        appComponent = DaggerAppComponent.builder()
                .appModule(new AppModule(this))
                .build();
    }

    public AppComponent getAppComponent() {
        return appComponent;
    }
}

ثبت کلاس Application در فایل مانیفست را فراموش نکنید.

<application
        android:name=".SampleApp"
</application>

گام ۳: تزریق کلاس‌های MVP

بالاخره می‌توانیم می‌توانیم به کمک @Inject کلاس‌های MVP را تزریق کنیم. تغییرات مورد نیاز باید در کلاس MainActivity صورت بگیرد. ما روش مقدار دهی کلاس‌های مدل و معرف را تغییر می‌دهیم. قدم اول تغییر اعلان متغیر MVP_Main.ProvidedPresenterOps است. باید آن را عمومی یا public اعلام کنیم و باید @Inject به آن اضافه کنیم.

@Inject
public MVP_Main.ProvidedPresenterOps mPresenter;

برای تنظیم MainActivityComponent کد زیر را اضافه کنید.

/**
 * Setup the {@link com.tinmegali.tutsmvp_sample.di.component.MainActivityComponent}
 * to instantiate and inject a {@link MainPresenter}
 */
private void setupComponent(){
    Log.d(TAG, "setupComponent");
    SampleApp.get(this)
            .getAppComponent()
            .getMainComponent(new MainActivityModule(this))
            .inject(this);
}

تنها چیزی که باقی مانده است مقداردهی یا بازمقداردهی معرف بر اساس وضعیتش در StateMaintainer است. برای این کار متد setupMVP() را تغییر داده و کدهای زیر را اضافه می‌کنیم.

/**
 * Setup Model View Presenter pattern.
 * Use a {@link StateMaintainer} to maintain the
 * Presenter and Model instances between configuration changes.
 */
private void setupMVP(){
    if ( mStateMaintainer.firstTimeIn() ) {
        initialize();
    } else {
        reinitialize();
    }
}

/**
 * Setup the {@link MainPresenter} injection and saves in <code>mStateMaintainer</code>
 */
private void initialize(){
    Log.d(TAG, "initialize");
    setupComponent();
    mStateMaintainer.put(MainPresenter.class.getSimpleName(), mPresenter);
}

/**
 * Recover {@link MainPresenter} from <code>mStateMaintainer</code> or creates
 * a new {@link MainPresenter} if the instance has been lost from <code>mStateMaintainer</code>
 */
private void reinitialize() {
    Log.d(TAG, "reinitialize");
    mPresenter = mStateMaintainer.get(MainPresenter.class.getSimpleName());
    mPresenter.setView(this);
    if ( mPresenter == null )
        setupComponent();
}

حالا کلیه عناصر MVP به طور مستقل از نمایشگر تنظیم می‌شوند و به کمک تزریق وابستگی کد ساختار بهتری دارد. می‌تواند کد را بیشتر از این هم ارتقا داد و از تزریق وابستگی برای تزریق کلاس‌های دیگر مانند DAO هم استفاده کنید.

۳- جلوگیری از اشتباهات متداول

من در اینجا تعدادی از خطاهای متداول را فهرست کرده‌ام که باید در زمان استفاده از MVP از آن‌ها اجتناب کنید:

  • همیشه قبل از صدا زدن نمایشگر چک کنید که نمایشگر آماده و در دسترس باشد. نمایشگر شدیداً وابسته به چرخه زندگی برنامه است و ممکن است در زمانی که شما درخواستی را به آن می‌فرستید، نابود شده باشد.
  • فراموش نکنید که پس از ساخت شدن دوباره نمایشگر، یک ارجاع جدید برایش بفرستید.
  • هر زمان که نمایشگر نابود شد، متد onDestroy() را در معرف صدا بزنید. بعضی وقت‌ها، باید رویدادهای onPause() و onStop() را هم به معرف اطلاع بدهید.
  • هر زمان که با نمایشگرهای پیچیده کار می‌کنید، سعی کنید از چند معرف استفاده کنید.
  • وقتی که از چند معرف استفاده می‌کنید، بهترین روش برای جابجایی اطلاعات بین آن‌ها استفاده از نوعی «گذرگاه رویداد» یا Event Bus است.
  • برای این که تا حد ممکن لایه نمایشگر را منفعل بسازید، از تزریق وابستگی برای ساخت لایه‌های معرف و مدل در بیرون از لایه نمایشگر استفاده کنید.

مؤخره

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

کد برنامه نمونه

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


Download

Facebooktwittergoogle_plusredditpinterestlinkedinmailFacebooktwittergoogle_plusredditpinterestlinkedinmail




3 فکر می‌کنند “آموزش اندروید-فصل ۲۸-۳: تست برنامه‌های MVP

  1. Koorush

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

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

    پاسخ

پاسخ دادن به Koorush لغو پاسخ

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *