Мы исследовали концепции шаблона представления модели в первой части этой серии и реализовали собственную версию шаблона во второй части . Пришло время копать немного глубже. В этом уроке мы сосредоточимся на следующих темах:
- настройка тестовой среды и написание модульных тестов для классов 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 в своих собственных проектах, протестировать его и даже внедрить внедрение зависимостей. Надеюсь, вам понравилось это путешествие так же, как и мне. Я надеюсь увидеть вас в ближайшее время.