Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Testable Android Apps using data binding and MVVM

3,113 views

Published on

How to write testable Android code using the Data Binding library and Model View ViewModel pattern

Published in: Software
  • Be the first to comment

Testable Android Apps using data binding and MVVM

  1. 1. Testable Android Apps using data binding and MVVM Fabio Collini
  2. 2. 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?
  3. 3. 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
  4. 4. GDG DevFest - Milano - October 2015 - @fabioCollini 4 1ROI and legacy code
  5. 5. GDG DevFest – Milano – October 2015 – @fabioCollini 5 Quick survey Do you write automated tests?
  6. 6. GDG DevFest – Milano – October 2015 – @fabioCollini 6
  7. 7. GDG DevFest – Milano – October 2015 – @fabioCollini 7 Return of Investment - ROI Net profit Investment
  8. 8. GDG DevFest – Milano – October 2015 – @fabioCollini 8 Legacy code Edit and pray Vs Cover and modify Legacy code is code without unit tests
  9. 9. 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 :(
  10. 10. 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
  11. 11. GDG DevFest - Milano - October 2015 - @fabioCollini 11 2Model View ViewModel
  12. 12. GDG DevFest – Milano – October 2015 – @fabioCollini 12 Testable code Data binding and MVVM
  13. 13. GDG DevFest – Milano – October 2015 – @fabioCollini 13 Model View ViewModel View ViewModel Model DataBinding
  14. 14. 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
  15. 15. GDG DevFest – Milano – October 2015 – @fabioCollini 15 mv2m https://github.com/fabioCollini/mv2m
  16. 16. GDG DevFest – Milano – October 2015 – @fabioCollini 16 NoteActivity NoteViewModel NoteModel note_detail.xml NoteDetailBinding DataBinding
  17. 17. GDG DevFest – Milano – October 2015 – @fabioCollini 17 View ViewModel RetrofitService onClick update binding Model View ViewModel RetrofitServiceModel request response binding
  18. 18. 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();
 
 //...
 }
  19. 19. 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);
 }
 }
  20. 20. 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>
  21. 21. 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>
  22. 22. 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>
  23. 23. 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);
 }
 }
  24. 24. 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();
 }
 });
 }
  25. 25. GDG DevFest – Milano – October 2015 – @fabioCollini 25 NoteViewModel public class NoteViewModel extends ViewModel<NoteModel> {
 
 //...
 
 @Override public NoteModel createDefaultModel() {
 return new NoteModel();
 }
 
 @Override public void resume() {
 if (!getModel().isLoaded()) {
 reloadData();
 }
 }
 
 public void reloadData() {
 }
 
 //...
 }
  26. 26. GDG DevFest - Milano - October 2015 - @fabioCollini 26 3JVM Unit tests
  27. 27. 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
  28. 28. GDG DevFest – Milano – October 2015 – @fabioCollini JVM Test 28 NoteActivity NoteViewModel NoteModel note_detail.xml NoteDetailBinding DataBinding
  29. 29. GDG DevFest – Milano – October 2015 – @fabioCollini 29 NoteViewModel.reloadData public 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);
 }
 }
 //...
 }
  30. 30. GDG DevFest – Milano – October 2015 – @fabioCollini 30 First test AssertJ @Test
 public 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();
 }
  31. 31. GDG DevFest – Milano – October 2015 – @fabioCollini 31 NoteLoader.singleton public 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);
 }
 }
 //...
 }
  32. 32. GDG DevFest – Milano – October 2015 – @fabioCollini 32 Dependency Injection public 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);
 }
 } //... }
  33. 33. 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;
 }
 }
  34. 34. GDG DevFest – Milano – October 2015 – @fabioCollini 34 Test with stub @Test
 public 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();
 }
  35. 35. 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
  36. 36. 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;
 }
 }
  37. 37. 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) {
 }
 }
  38. 38. 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;
 }
 }
  39. 39. GDG DevFest – Milano – October 2015 – @fabioCollini 39 Test with spy @Test
 public 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);
 }
  40. 40. GDG DevFest - Milano - October 2015 - @fabioCollini 40 4Mockito
  41. 41. GDG DevFest – Milano – October 2015 – @fabioCollini 41 Mockito @Test
 public 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");
 }
  42. 42. GDG DevFest – Milano – October 2015 – @fabioCollini MockLoader MockLoaderNoteLoader NoteLoader 42 ViewModel initAndResume update Model request response JVM Test ViewModel ModelJVM Test assert when().thenReturn()
  43. 43. GDG DevFest – Milano – October 2015 – @fabioCollini 43 Mockito @Test
 public 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));
 }

  44. 44. GDG DevFest – Milano – October 2015 – @fabioCollini MockMessage Manager Message Manager MockMessage Manager Message Manager 44 ViewModel MockSaver save showMessage Model request response JVM Test ViewModel MockSaverModelJVM Test verify verify NoteSaver NoteSaver
  45. 45. 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"));
 }
 //...
 }
  46. 46. 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"));
 } 
 //...
 }
  47. 47. 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)
  48. 48. 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);
 }
 }
 });
 }
  49. 49. 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);
 }
 });
 }

  50. 50. 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;
 
 //...
 }
  51. 51. GDG DevFest - Milano - October 2015 - @fabioCollini 51 5Espresso
  52. 52. GDG DevFest – Milano – October 2015 – @fabioCollini 52 NoteLoader public 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;
 }
 
 //...
 }
  53. 53. 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
  54. 54. GDG DevFest – Milano – October 2015 – @fabioCollini 54 Reload test @Test
 public 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()));
 }

  55. 55. GDG DevFest – Milano – October 2015 – @fabioCollini 55 View ViewModel MockLoader perform(click()) update binding Model request response EspressoTest View ViewModel MockLoaderModelEspressoTest onView verify NoteLoader NoteLoader when().thenReturn() onClick binding
  56. 56. 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
  57. 57. 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
  58. 58. GDG DevFest – Milano – October 2015 – @fabioCollini 58 Thanks for your attention! androidavanzato.it Questions?

×