Upload
fabio-collini
View
1.961
Download
2
Embed Size (px)
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 && !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?