Статьи

Тестирование и внедрение зависимостей с представлением модели на Android

Мы исследовали концепции шаблона представления модели в первой части этой серии и реализовали собственную версию шаблона во второй части . Пришло время копать немного глубже. В этом уроке мы сосредоточимся на следующих темах:

  • настройка тестовой среды и написание модульных тестов для классов MVP
  • реализация шаблона MVP с использованием внедрения зависимостей с помощью Dagger 2
  • мы обсуждаем общие проблемы, которых следует избегать при использовании MVP на Android

Одним из самых больших преимуществ принятия шаблона MVP является то, что он упрощает модульное тестирование. Итак, давайте напишем тесты для классов Model и Presenter, которые мы создали и реализовали в последней части этой серии. Мы будем запускать наши тесты, используя Robolectric , инфраструктуру модульных тестов, которая предоставляет множество полезных заглушек для классов Android. Для создания фиктивных объектов мы будем использовать Mockito , который позволяет нам проверять, были ли вызваны определенные методы.

Отредактируйте файл 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 из списка.

Выберите JUnit

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

Настройте конфигурацию

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

Настройте конфигурацию

Давайте начнем наши тесты с класса 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));
}

Теперь вы можете запустить тест модели и проверить результаты. Не стесняйтесь проверять другие аспекты класса.

Теперь давайте сосредоточимся на тестировании докладчика. Для этого теста нам также понадобится 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));
}

Dependency Injection — отличный инструмент для разработчиков. Если вы не знакомы с внедрением зависимостей, я настоятельно рекомендую прочитать статью Керри на эту тему.

Внедрение зависимостей — это стиль конфигурации объекта, в котором поля и соавторы объекта задаются внешней сущностью. Другими словами, объекты настраиваются внешним объектом. Внедрение зависимостей — это альтернатива настройке объекта. Якоб Дженков

В этом примере внедрение зависимостей позволяет создавать Model и Presenter вне View, делая слои MVP более свободными и увеличивая разделение интересов.

Мы используем Dagger 2 , потрясающую библиотеку от Google, чтобы помочь нам с внедрением зависимостей. Хотя установка проста, у dagger 2 есть много интересных опций, и это довольно сложная библиотека.

Мы сконцентрируемся только на соответствующих частях библиотеки для реализации MVP и не будем подробно рассказывать о библиотеке. Если вы хотите узнать больше о Dagger, прочитайте руководство Керри или документацию, предоставленную Google.

Начните с обновления файла 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’
    // …
}

Синхронизируйте проект и дождитесь завершения операции.

Давайте начнем с создания @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>

Наконец, мы можем @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.

Я перечислил ряд распространенных проблем, которые следует избегать при использовании шаблона Model View Presenter.

  • Всегда проверяйте, доступен ли просмотр, прежде чем вызывать его. Представление привязано к жизненному циклу приложения и может быть уничтожено во время вашего запроса.
  • Не забудьте передать новую ссылку из представления, когда она воссоздается.
  • onDestroy() в Presenter каждый раз, когда представление уничтожается. В некоторых случаях может потребоваться сообщить докладчику о onStop или onPause .
  • Рассмотрите возможность использования нескольких презентаторов при работе со сложными видами.
  • При использовании нескольких докладчиков самый простой способ передачи информации между ними — использование некоторой шины событий.
  • Чтобы сохранить слой View как можно более пассивным, рассмотрите возможность использования внедрения зависимостей для создания слоев Presenter и Model за пределами View.

Вы достигли конца этой серии, в которой мы исследовали шаблон Presenter Model Viewer. Теперь вы должны иметь возможность реализовать шаблон MVP в своих собственных проектах, протестировать его и даже внедрить внедрение зависимостей. Надеюсь, вам понравилось это путешествие так же, как и мне. Я надеюсь увидеть вас в ближайшее время.