Мы исследовали концепции шаблона представления модели в первой части этой серии и реализовали собственную версию шаблона во второй части . Пришло время копать немного глубже. В этом уроке мы сосредоточимся на следующих темах:
- настройка тестовой среды и написание модульных тестов для классов MVP
- реализация шаблона MVP с использованием внедрения зависимостей с помощью Dagger 2
- мы обсуждаем общие проблемы, которых следует избегать при использовании MVP на Android
1. Модульное тестирование
Одним из самых больших преимуществ принятия шаблона MVP является то, что он упрощает модульное тестирование. Итак, давайте напишем тесты для классов Model и Presenter, которые мы создали и реализовали в последней части этой серии. Мы будем запускать наши тесты, используя Robolectric , инфраструктуру модульных тестов, которая предоставляет множество полезных заглушек для классов Android. Для создания фиктивных объектов мы будем использовать Mockito , который позволяет нам проверять, были ли вызваны определенные методы.
Шаг 1: Настройка
Отредактируйте файл build.gradle вашего модуля приложения и добавьте следующие зависимости.
| 1 2 3 4 5 6 7 8 | 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 / [имя-пакета] / [имя-приложения] . Затем создайте конфигурацию отладки для запуска набора тестов. Нажмите Редактировать конфигурации … вверху.

Нажмите кнопку + и выберите JUnit из списка.

Установите для Рабочего каталога значение $ MODULE_DIR $ .

Мы хотим, чтобы эта конфигурация запускала все модульные тесты. Установите для параметра « Вид теста» значение « Все в пакете» и введите имя пакета в поле « Пакет» .

  Шаг 2: Тестирование модели 
 Давайте начнем наши тесты с класса Model.  RobolectricGradleTestRunner.class тест выполняется с использованием RobolectricGradleTestRunner.class , который предоставляет ресурсы, необходимые для тестирования определенных операций Android.  Важно @Cofing следующими параметрами: 
| 1 2 3 4 5 6 | @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 . 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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.     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); } | 
Настало время проверить методы Модели.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | // 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)); } | 
Теперь вы можете запустить тест модели и проверить результаты. Не стесняйтесь проверять другие аспекты класса.
  Шаг 3: Тестирование докладчика 
  Теперь давайте сосредоточимся на тестировании докладчика.  Для этого теста нам также понадобится Robolectric, чтобы использовать несколько классов Android, таких как AsyncTask .  Конфигурация очень похожа на тестирование модели.  Мы используем макеты View и Model для проверки вызовов методов и определения возвращаемых значений. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | @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);     } } | 
  Чтобы протестировать методы Presenter, давайте начнем с операции clickNewNote() , которая отвечает за создание новой заметки и ее регистрацию в базе данных с помощью AsyncTask . 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | @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() возвращает ошибку. 
| 1 2 3 4 5 6 7 8 9 | @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() , рассматривая как успешный, так и неудачный результат. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @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)); } | 
2. Инъекция зависимости с помощью кинжала 2
Dependency Injection — отличный инструмент для разработчиков. Если вы не знакомы с внедрением зависимостей, я настоятельно рекомендую прочитать статью Керри на эту тему.
Внедрение зависимостей — это стиль конфигурации объекта, в котором поля и соавторы объекта задаются внешней сущностью. Другими словами, объекты настраиваются внешним объектом. Внедрение зависимостей — это альтернатива настройке объекта. — Якоб Дженков
В этом примере внедрение зависимостей позволяет создавать Model и Presenter вне View, делая слои MVP более свободными и увеличивая разделение интересов.
Мы используем Dagger 2 , потрясающую библиотеку от Google, чтобы помочь нам с внедрением зависимостей. Хотя установка проста, у dagger 2 есть много интересных опций, и это довольно сложная библиотека.
Мы сконцентрируемся только на соответствующих частях библиотеки для реализации MVP и не будем подробно рассказывать о библиотеке. Если вы хотите узнать больше о Dagger, прочитайте руководство Керри или документацию, предоставленную Google.
Шаг 1: Настройка Dagger 2
Начните с обновления файла build.gradle проекта, добавив зависимость.
| 1 2 3 4 | dependencies {     // …     classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.8’ } | 
Затем отредактируйте файл build.dagger проекта, как показано ниже.
| 1 2 3 4 5 6 7 8 9 | 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’     // … } | 
Синхронизируйте проект и дождитесь завершения операции.
Шаг 2: Реализация MVP с помощью Dagger 2
  Давайте начнем с создания @Scope для классов Activity .  Создайте @annotation с именем области. 
| 1 2 3 | @Scope public @interface ActivityScope { } | 
  Затем мы работаем над @Module для MainActivity .  Если у вас есть несколько действий, вы должны предоставить @Module для каждого Activity . 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | @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 , которое нам еще нужно создать. 
| 1 2 3 4 5 | @ActivityScope @Subcomponent( modules = MainActivityModule.class ) public interface MainActivityComponent {     MainActivity inject(MainActivity activity); } | 
  Мы должны создать @Module и @Component для Application . 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | @Module public class AppModule {     private Application application;     public AppModule(Application application) {         this.application = application;     }     @Provides     @Singleton     public Application providesApplication() {         return application;     } } | 
| 1 2 3 4 5 6 | @Singleton @Component( modules = AppModule.class) public interface AppComponent {     Application application();     MainActivityComponent getMainComponent(MainActivityModule module); } | 
  Наконец, нам нужен класс Application для инициализации внедрения зависимости. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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;     } } | 
Не забудьте включить имя класса в манифест проекта.
| 1 2 3 4 | <application         android:name=».SampleApp»         …. </application> | 
  Шаг 3: Внедрение классов MVP 
  Наконец, мы можем @Inject наши классы MVP.  Изменения, которые нам нужно сделать, делаются в классе MainActivity .  Мы меняем способ инициализации модели и докладчика.  Первым шагом является изменение MVP_Main.ProvidedPresenterOps переменной MVP_Main.ProvidedPresenterOps .  Он должен быть public и нам нужно добавить аннотацию @Inject . 
| 1 2 | @Inject public MVP_Main.ProvidedPresenterOps mPresenter; | 
  Чтобы настроить MainActivityComponent , добавьте следующее: 
| 01 02 03 04 05 06 07 08 09 10 11 | /**  * 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); } | 
  Все, что нам нужно сделать сейчас, это инициализировать или повторно инициализировать Presenter, в зависимости от его состояния на StateMaintainer .  Измените метод setupMVP() и добавьте следующее: 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | /**  * 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.
3. Как избежать общих проблем
Я перечислил ряд распространенных проблем, которые следует избегать при использовании шаблона Model View Presenter.
- Всегда проверяйте, доступен ли просмотр, прежде чем вызывать его. Представление привязано к жизненному циклу приложения и может быть уничтожено во время вашего запроса.
- Не забудьте передать новую ссылку из представления, когда она воссоздается.
-   onDestroy()в Presenter каждый раз, когда представление уничтожается. В некоторых случаях может потребоваться сообщить докладчику оonStopилиonPause.
- Рассмотрите возможность использования нескольких презентаторов при работе со сложными видами.
- При использовании нескольких докладчиков самый простой способ передачи информации между ними — использование некоторой шины событий.
- Чтобы сохранить слой View как можно более пассивным, рассмотрите возможность использования внедрения зависимостей для создания слоев Presenter и Model за пределами View.
Вывод
Вы достигли конца этой серии, в которой мы исследовали шаблон Presenter Model Viewer. Теперь вы должны иметь возможность реализовать шаблон MVP в своих собственных проектах, протестировать его и даже внедрить внедрение зависимостей. Надеюсь, вам понравилось это путешествие так же, как и мне. Я надеюсь увидеть вас в ближайшее время.