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. GDG DevFest - Milano - October 2015 - @fabioCollini 4
1ROI and legacy code
5. GDG DevFest – Milano – October 2015 – @fabioCollini 5
Quick survey
Do you write automated tests?
6. GDG DevFest – Milano – October 2015 – @fabioCollini 6
7. GDG DevFest – Milano – October 2015 – @fabioCollini 7
Return of Investment - ROI
Net profit
Investment
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. 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. 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. GDG DevFest - Milano - October 2015 - @fabioCollini 11
2Model View ViewModel
12. GDG DevFest – Milano – October 2015 – @fabioCollini 12
Testable code
Data binding and MVVM
13. GDG DevFest – Milano – October 2015 – @fabioCollini 13
Model View ViewModel
View
ViewModel
Model
DataBinding
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. GDG DevFest – Milano – October 2015 – @fabioCollini 15
mv2m
https://github.com/fabioCollini/mv2m
16. GDG DevFest – Milano – October 2015 – @fabioCollini 16
NoteActivity
NoteViewModel
NoteModel
note_detail.xml
NoteDetailBinding
DataBinding
17. GDG DevFest – Milano – October 2015 – @fabioCollini 17
View ViewModel RetrofitService
onClick
update
binding
Model
View ViewModel RetrofitServiceModel
request
response
binding
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. 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);
}
}
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. 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. 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. GDG DevFest - Milano - October 2015 - @fabioCollini 26
3JVM Unit tests
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. GDG DevFest – Milano – October 2015 – @fabioCollini
JVM Test
28
NoteActivity
NoteViewModel
NoteModel
note_detail.xml
NoteDetailBinding
DataBinding
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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. GDG DevFest - Milano - October 2015 - @fabioCollini 40
4Mockito
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. 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. 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. 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. 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. 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. 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)
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. 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. GDG DevFest – Milano – October 2015 – @fabioCollini 58
Thanks for your attention!
androidavanzato.it
Questions?