Testable Android Apps using data binding and MVVM

  • View
    1.961

  • Download
    2

  • Category

    Software

Preview:

Citation preview

Testable Android Apps using data binding and MVVM

Fabio Collini

GDG DevFest – Milano – October 2015 – @fabioCollini 2

Ego slide@fabioCollini linkedin.com/in/fabiocollini Folder Organizer cosenonjaviste.it

nana bianca Freapp instal.com Rain tomorrow?

GDG DevFest – Milano – October 2015 – @fabioCollini 3

Agenda

1. ROI and Legacy code 2. Model View ViewModel 3. JVM Unit tests 4. Mockito 5. Espresso

GDG DevFest - Milano - October 2015 - @fabioCollini 4

1ROI and legacy code

GDG DevFest – Milano – October 2015 – @fabioCollini 5

Quick survey

Do you write automated tests?

GDG DevFest – Milano – October 2015 – @fabioCollini 6

GDG DevFest – Milano – October 2015 – @fabioCollini 7

Return of Investment - ROI

Net profit Investment

GDG DevFest – Milano – October 2015 – @fabioCollini 8

Legacy code

Edit and pray Vs

Cover and modify

Legacy code is code without unit tests

GDG DevFest – Milano – October 2015 – @fabioCollini 9

Test After Development

Write the feature implementation Do some manual testing Try to write automatic tests Modify the initial implementation to test it

“Standard” Android code is not testable :(

GDG DevFest – Milano – October 2015 – @fabioCollini 10

Legacy code dilemma

When we change code, we should have tests in place.

To put tests in place, we often have to change code.

Michael Feathers

GDG DevFest - Milano - October 2015 - @fabioCollini 11

2Model View ViewModel

GDG DevFest – Milano – October 2015 – @fabioCollini 12

Testable code

Data binding and MVVM

GDG DevFest – Milano – October 2015 – @fabioCollini 13

Model View ViewModel

View

ViewModel

Model

DataBinding

GDG DevFest – Milano – October 2015 – @fabioCollini 14

Android Model View ViewModel

View

ViewModel

Model

DataBinding

Retained on configuration change

Saved in Activity or Fragment state

Activity or Fragment

GDG DevFest – Milano – October 2015 – @fabioCollini 15

mv2m

https://github.com/fabioCollini/mv2m

GDG DevFest – Milano – October 2015 – @fabioCollini 16

NoteActivity

NoteViewModel

NoteModel

note_detail.xml NoteDetailBinding

DataBinding

GDG DevFest – Milano – October 2015 – @fabioCollini 17

View ViewModel RetrofitService

onClick

updatebinding

Model

View ViewModel RetrofitServiceModel

request

response

binding

GDG DevFest – Milano – October 2015 – @fabioCollini 18

NoteModel

Saved on Activity state

public class NoteModel implements Parcelable { private long noteId; private ObservableBoolean error = new ObservableBoolean(); private ObservableString title = new ObservableString(); private ObservableString text = new ObservableString(); private ObservableInt titleError = new ObservableInt(); private ObservableInt textError = new ObservableInt(); //...}

GDG DevFest – Milano – October 2015 – @fabioCollini 19

NoteActivity

public class NoteActivity extends ViewModelActivity<NoteViewModel> { @Override public NoteViewModel createViewModel() { return new NoteViewModel(/* .. */); } @Override protected void onCreate(Bundle state) { super.onCreate(state); NoteDetailBinding binding = DataBindingUtil.setContentView(this, R.layout.note_detail); binding.setViewModel(viewModel); } }

GDG DevFest – Milano – October 2015 – @fabioCollini 20

note_detail.xml<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="viewModel" type="it.cosenonjaviste.core.NoteViewModel"/> </data> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent"> <!-- ... --> </FrameLayout> </layout>

GDG DevFest – Milano – October 2015 – @fabioCollini 21

note_detail.xml<LinearLayout app:visible="@{viewModel.loading}">

<ProgressBar />

<TextView /></LinearLayout> <LinearLayout app:visible=“@{viewModel.model.error}"> <TextView android:text="@string/error_loading_note"/> <Button android:text=“@string/retry"/></LinearLayout> <ScrollView app:visible="@{!viewModel.loading &amp;&amp; !viewModel.model.error}"> <!-- ... --></ScrollView>

GDG DevFest – Milano – October 2015 – @fabioCollini 22

note_detail.xml<LinearLayout> <android.support.design.widget.TextInputLayout app:error=“@{viewModel.model.titleError}"> <EditText app:binding=“@{viewModel.model.title}" /> </android.support.design.widget.TextInputLayout>

<!-- ... --> <RelativeLayout> <Button android:enabled="@{!viewModel.sending}" app:onClick="@{viewModel.save}"/>

<ProgressBar app:visible=“@{viewModel.sending}" />

</RelativeLayout> </LinearLayout>

GDG DevFest – Milano – October 2015 – @fabioCollini 23

app:binding

@BindingAdapter({"app:binding"})public static void bindEditText(EditText view, final ObservableString observableString) { if (view.getTag(R.id.binded) == null) { view.setTag(R.id.binded, true); view.addTextChangedListener(new TextWatcherAdapter() { @Override public void onTextChanged( CharSequence s, int st, int b, int c) { observableString.set(s.toString()); } }); } String newValue = observableString.get(); if (!view.getText().toString().equals(newValue)) { view.setText(newValue); }}

GDG DevFest – Milano – October 2015 – @fabioCollini 24

app:visible app:onClick

@BindingAdapter({"app:visible"})public static void bindVisible(View view, boolean b) { view.setVisibility(b ? View.VISIBLE : View.INVISIBLE); } @BindingAdapter({"app:onClick"})public static void bindOnClick(View view, final Runnable listener) { view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { listener.run(); } });}

GDG DevFest – Milano – October 2015 – @fabioCollini 25

NoteViewModelpublic class NoteViewModel extends ViewModel<NoteModel> { //... @Override public NoteModel createDefaultModel() { return new NoteModel(); } @Override public void resume() { if (!getModel().isLoaded()) { reloadData(); } } public void reloadData() { } //...}

GDG DevFest - Milano - October 2015 - @fabioCollini 26

3JVM Unit tests

GDG DevFest – Milano – October 2015 – @fabioCollini 27

Instrumentation tests run on a device (real or emulated)

high code coverage

Vs JVM tests

fast low code coverage

GDG DevFest – Milano – October 2015 – @fabioCollini

JVM Test

28

NoteActivity

NoteViewModel

NoteModel

note_detail.xml NoteDetailBinding

DataBinding

GDG DevFest – Milano – October 2015 – @fabioCollini 29

NoteViewModel.reloadDatapublic class NoteViewModel extends ViewModel<NoteModel> { //...

@Override public void resume() { if (!getModel().isLoaded()) { reloadData(); } } public void reloadData() { try { Note note = NoteLoader.singleton().load(); getModel().update(note); } catch (Exception e) { getModel().getError().set(true); } } //...}

GDG DevFest – Milano – October 2015 – @fabioCollini 30

First test

AssertJ

@Testpublic void testLoadData() { NoteViewModel viewModel = new NoteViewModel(); NoteModel model = viewModel.initAndResume(); assertThat(model.getTitle().get()).isEqualTo("???"); assertThat(model.getText().get()).isEqualTo("???"); assertThat(model.getError().get()).isFalse(); }

GDG DevFest – Milano – October 2015 – @fabioCollini 31

NoteLoader.singletonpublic class NoteViewModel extends ViewModel<NoteModel> { //...

@Override public void resume() { if (!getModel().isLoaded()) { reloadData(); } } public void reloadData() { try { Note note = NoteLoader.singleton().load(); getModel().update(note); } catch (Exception e) { getModel().getError().set(true); } } //...}

GDG DevFest – Milano – October 2015 – @fabioCollini 32

Dependency Injectionpublic class NoteViewModel extends ViewModel<NoteModel, NoteView> {

private NoteLoader noteLoader;

public NoteViewModel(NoteLoader noteLoader) { this.noteLoader = noteLoader; } public void reloadData() { try { Note note = noteLoader.load(); getModel().update(note); } catch (Exception e) { getModel().getError().set(true); } } //... }

GDG DevFest – Milano – October 2015 – @fabioCollini 33

NoteLoaderStub

public class NoteLoaderStub implements NoteLoader { private Note note; public NoteLoaderStub(Note note) { this.note = note; } @Override public Note load() { return note; } }

GDG DevFest – Milano – October 2015 – @fabioCollini 34

Test with stub

@Testpublic void testLoadData() { NoteLoaderStub stub = new NoteLoaderStub(new Note(1, "a", "b")); NoteViewModel viewModel = new NoteViewModel(stub); NoteModel model = viewModel.initAndResume(); assertThat(model.getTitle().get()).isEqualTo("a"); assertThat(model.getText().get()).isEqualTo("b"); assertThat(model.getError().get()).isFalse(); }

GDG DevFest – Milano – October 2015 – @fabioCollini

public void save() { NoteModel model = getModel(); boolean titleValid = checkMandatory( model.getTitle(), model.getTitleError()); boolean textValid = checkMandatory( model.getText(), model.getTextError()); if (titleValid && textValid) { try { noteSaver.save( model.getNoteId(), model.getTitle().get(), model.getText().get());

messageManager.showMessage(R.string.note_saved); } catch (RetrofitError e) { messageManager.showMessage( R.string.error_saving_note); } }}

35

save method

Dependency Injection

Dependency Injection

GDG DevFest – Milano – October 2015 – @fabioCollini 36

SnackbarMessageManager

public class SnackbarMessageManager implements MessageManager { private Activity activity; @Override public void showMessage(int message) { if (activity != null) { Snackbar.make( activity.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG ).show(); } } @Override public void setActivity(Activity activity) { this.activity = activity; }}

GDG DevFest – Milano – October 2015 – @fabioCollini 37

MessageManagerSpy

public class MessageManagerSpy implements MessageManager { public int message; @Override public void showMessage(int message) { this.message = message; } @Override public void setActivity(Activity activity) { }}

GDG DevFest – Milano – October 2015 – @fabioCollini 38

NoteSaverSpy

public class NoteSaverSpy implements NoteSaver { public long id; public String title; public String text; @Override public Response save( long id, String title, String text) { this.id = id; this.title = title; this.text = text; return null; } }

GDG DevFest – Milano – October 2015 – @fabioCollini 39

Test with spy@Testpublic void testSaveData() { NoteLoaderStub stub = new NoteLoaderStub(new Note(1, "a", "b")); NoteSaverSpy saverSpy = new NoteSaverSpy(); MessageManagerSpy messageSpy = new MessageManagerSpy(); NoteViewModel viewModel = new NoteViewModel( stub, saverSpy, messageSpy); NoteModel model = viewModel.initAndResume(); model.getTitle().set("newTitle"); model.getText().set("newText"); viewModel.save(); assertThat(saverSpy.id).isEqualTo(1L); assertThat(saverSpy.title).isEqualTo("newTitle"); assertThat(saverSpy.text).isEqualTo("newText"); assertThat(messageSpy.message) .isEqualTo(R.string.note_saved); }

GDG DevFest - Milano - October 2015 - @fabioCollini 40

4Mockito

GDG DevFest – Milano – October 2015 – @fabioCollini 41

Mockito@Testpublic void testLoadData() { NoteLoader noteLoader = Mockito.mock(NoteLoader.class); NoteSaver noteSaver = Mockito.mock(NoteSaver.class); MessageManager messageManager = Mockito.mock(MessageManager.class); NoteViewModel viewModel = new NoteViewModel( noteLoader, noteSaver, messageManager); when(noteLoader.load()) .thenReturn(new Note(123, "title", "text"));

NoteModel model = viewModel.initAndResume(); assertThat(model.getTitle().get()).isEqualTo("title"); assertThat(model.getText().get()).isEqualTo("text"); }

GDG DevFest – Milano – October 2015 – @fabioCollini

MockLoader

MockLoaderNoteLoader

NoteLoader

42

ViewModel

initAndResume

update

Model

request

response

JVM Test

ViewModel ModelJVM Test

assert

when().thenReturn()

GDG DevFest – Milano – October 2015 – @fabioCollini 43

Mockito@Testpublic void testSaveData() { //... NoteModel model = viewModel.initAndResume(); model.getTitle().set("newTitle"); model.getText().set("newText"); viewModel.save(); verify(noteSaver) .save(eq(123L), eq("newTitle"), eq("newText")); verify(messageManager) .showMessage(eq(R.string.note_saved)); }

GDG DevFest – Milano – October 2015 – @fabioCollini

MockMessage ManagerMessage Manager

MockMessage ManagerMessage Manager

44

ViewModel MockSaver

save

showMessage

Model

request

response

JVM Test

ViewModel MockSaverModelJVM Test

verify

verify

NoteSaver

NoteSaver

GDG DevFest – Milano – October 2015 – @fabioCollini 45

SetUp method public class NoteViewModelTest { private NoteLoader noteLoader; private NoteSaver noteSaver; private MessageManager messageManager; private NoteViewModel viewModel; @Before public void setUp() { noteLoader = Mockito.mock(NoteLoader.class); noteSaver = Mockito.mock(NoteSaver.class); messageManager = Mockito.mock(MessageManager.class); viewModel = new NoteViewModel( noteLoader, noteSaver, messageManager);

when(noteLoader.load()) .thenReturn(new Note(123, "title", "text")); } //...}

GDG DevFest – Milano – October 2015 – @fabioCollini 46

@Mock and @InjectMocks@RunWith(MockitoJUnitRunner.class) public class NoteViewModelTest { @Mock NoteLoader noteLoader; @Mock NoteSaver noteSaver; @Mock MessageManager messageManager; @InjectMocks NoteViewModel viewModel; @Before public void setUp() throws Exception { when(noteLoader.load()) .thenReturn(new Note(123, "title", "text")); } //...}

GDG DevFest – Milano – October 2015 – @fabioCollini 47

Dagger

A fast dependency injector for Android and Java v1 developed at Square https://github.com/square/dagger

v2 developed at Google https://github.com/google/dagger

Configuration using annotations and Java classes Based on annotation processing (no reflection)

GDG DevFest – Milano – October 2015 – @fabioCollini 48

Background executor

if (titleValid && textValid) { sending.set(true); backgroundExecutor.execute(new Runnable() { @Override public void run() { try { noteSaver.save(getModel().getNoteId(), getModel().getTitle().get(), getModel().getText().get()); hideSendProgressAndShowMessage( R.string.note_saved); } catch (RetrofitError e) { hideSendProgressAndShowMessage( R.string.error_saving_note); } } }); }

GDG DevFest – Milano – October 2015 – @fabioCollini 49

Ui Executor

private void hideSendProgressAndShowMessage(final int msg) { uiExecutor.execute(new Runnable() { @Override public void run() { messageManager.showMessage(msg); sending.set(false); } }); }

GDG DevFest – Milano – October 2015 – @fabioCollini 50

Test using single thread@RunWith(MockitoJUnitRunner.class) public class NoteViewModelTest { @Mock NoteView view; @Mock NoteLoader noteLoader; @Mock NoteSaver noteSaver; @Spy Executor executor = new Executor() { @Override public void execute(Runnable command) { command.run(); } }; @InjectMocks NoteViewModel viewModel; //...}

GDG DevFest - Milano - October 2015 - @fabioCollini 51

5Espresso

GDG DevFest – Milano – October 2015 – @fabioCollini 52

NoteLoaderpublic class NoteLoader { private static NoteLoader instance; public static NoteLoader singleton() { if (instance == null) { instance = new NoteLoader(); } return instance; } private NoteLoader() { } @VisibleForTesting public static void setInstance(NoteLoader instance) { NoteLoader.instance = instance; } //...}

GDG DevFest – Milano – October 2015 – @fabioCollini

public class NoteActivityTest { @Rule public ActivityTestRule<NoteActivity> rule = new ActivityTestRule<>(NoteActivity.class, false, false); private NoteLoader noteLoader; @Before public void setUp() throws Exception { noteLoader = Mockito.mock(NoteLoader.class); NoteLoader.setInstance(noteLoader); }

//...}

53

NoteActivityTest

GDG DevFest – Milano – October 2015 – @fabioCollini 54

Reload test@Testpublic void testReloadAfterError() { when(noteLoader.load()) .thenThrow( RetrofitError.networkError("url", new IOException())) .thenReturn(new Note(123, "aaa", "bbb")); rule.launchActivity(null); onView(withText(R.string.retry)).perform(click()); onView(withText(“aaa")) .check(matches(isDisplayed())); onView(withText(“bbb")) .check(matches(isDisplayed())); }

GDG DevFest – Milano – October 2015 – @fabioCollini 55

View ViewModel MockLoader

perform(click())

updatebinding

Model

requestresponse

EspressoTest

View ViewModel MockLoaderModelEspressoTest

onView

verify

NoteLoader

NoteLoader

when().thenReturn()

onClickbinding

GDG DevFest – Milano – October 2015 – @fabioCollini 56

Android Model View ViewModel

Activity (or Fragment) is the View All the business logic is in the ViewModel ViewModel is managed using Dependency Injection Model is the Activity (or Fragment) state ViewModel is retained on configuration change ViewModel is testable using a JVM test

GDG DevFest – Milano – October 2015 – @fabioCollini 57

Links

mockito.org joel-costigliola.github.io/assertj

Jay Fields - Working Effectively with Unit Tests Michael Feathers - Working Effectively with Legacy Code

medium.com/@fabioCollini/android-data-binding-f9f9d3afc761

github.com/fabioCollini/mv2m github.com/commit-non-javisti/CoseNonJavisteAndroidApp

GDG DevFest – Milano – October 2015 – @fabioCollini 58

Thanks for your attention!

androidavanzato.it

Questions?