Android Architecture Components - A Testable Approach to Android Development

Android Architecture Components - A Testable Approach to Android Development

Developing Android Apps has notoriously been a difficult task. Previously, Google refrained from giving advice as to how you should go about architecting your Android applications and it has been “left it up to the reader” to decide how. This lead to apps having a lot of bloat in their activities and being really difficult to test. This has now changed since Google I/O 2017 where the new Android Architecture components were introduced. The libraries aim to make developing and maintaining your Android apps a lot easier. In this presentation, Rebecca will present the components and dive into detail around the different types of tests you should write (and where) for your newly architected Android application.

Rebecca Franks is an Android Engineer at Over, currently building the Android version of the already popular photo editing iOS app. She is a Google Developer Expert for Android and she enjoys public speaking by frequently speaking at conferences and local meetups. Her blog has been featured multiple times on Android Weekly and she loves travelling the world.

2a37bf1e025cc1523124774c760df91a?s=128

Rebecca Franks

March 27, 2018
Tweet

Transcript

  1. @riggaroo Android Architecture Components A Testable Approach to Android Development

    Rebecca Franks
  2. HELLO! I am Rebecca Franks Android Developer Google Developer Expert

    @riggaroo
  3. What are the Android Architecture Components? Powerful APIs that are

    testable " Improve app quality ✅ Address common issues Best practice guidelines %
  4. @riggaroo Components - Room - ViewModels - LiveData * -

    Lifecycle * - Paging Library * - …
  5. Architecting an App

  6. bit.ly/android-arch bit.ly/android-arch-kotlin

  7. MVVM Model, View, ViewModel

  8. MVVM View ViewModel Model

  9. View ViewModel Model Retrofit API Room Database Web Service SQLite

    MVVM
  10. View ViewModel Model Retrofit API Room Database Web Service SQLite

    MVVM
  11. Room - SQLite Object Mapper

  12. Creating an Entity @Entity(tableName = TABLE_NAME) public class Event {

    @PrimaryKey(autoGenerate = true) private int id; private String name; private LocalDateTime date; public int getId() { return id; } public String getName() { return name; } public LocalDateTime getDate() { return date; } }
  13. Creating a DAO @Dao public interface EventDao { @Insert(onConflict =

    OnConflictStrategy.REPLACE) void addEvent(Event event); @Query("SELECT * FROM " + Event.TABLE_NAME + " WHERE " + Event.DATE_FIELD + " > :minDate") LiveData<List<Event>> getEvents(LocalDateTime minDate); }
  14. Creating a Database @Database(entities = {Event.class}, version = 1) public

    abstract class EventDatabase extends RoomDatabase { public abstract EventDao eventDao(); }
  15. Create an instance of the DB @Provides @Singleton EventDatabase providesEventDatabase(CountdownApplication

    context) { return Room.databaseBuilder(context, EventDatabase.class, “event.db").build(); }
  16. Model

  17. View ViewModel Model Retrofit API Room Database Web Service SQLite

    MVVM
  18. Model Model layer follows Repository pattern ie EventRepository, LoginRepository MVVM

    - Model
  19. Repository

  20. Repository Business Logic, Caching Plain object No Android Dependencies

  21. public class EventRepositoryImpl implements EventRepository { @Inject EventDao eventDao; public

    EventRepositoryImpl(EventDao eventDao) { this.eventDao = eventDao; } @Override public Completable addEvent(Event event) { if (event == null){ return Completable.error(new IllegalArgumentException("Event cannot be null")); } return Completable.fromAction(() -> eventDao.addEvent(event)); } }
  22. public class EventRepositoryImpl implements EventRepository { @Inject EventDao eventDao; public

    EventRepositoryImpl(EventDao eventDao) { this.eventDao = eventDao; } @Override public Completable addEvent(Event event) { if (event == null){ return Completable.error(new IllegalArgumentException("Event cannot be null")); } return Completable.fromAction(() -> eventDao.addEvent(event)); } } Completable is used when the Observable has to do some task without emitting a value.
  23. ViewModel

  24. View ViewModel Model Retrofit API Room Database Web Service SQLite

    MVVM
  25. ViewModel AddEventViewModel, EventListViewModel MVVM - ViewModel

  26. What is a ViewModel? - The VM in MVVM architecture

    - Store & manage UI-related data - Data survives config changes - No leaks
  27. public class AddEventViewModel extends ViewModel { @Inject EventRepository eventRepository; private

    String eventName; private String eventDescription; private LocalDateTime eventDateTime ; @Inject AddEventViewModel() { eventDateTime = LocalDateTime.now(); } void addEvent() { Event event = new Event(0, eventName, eventDescription, eventDateTime); eventRepository.addEvent(event).observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new CompletableObserver() { @Override public void onSubscribe(Disposable d) { } Creating your ViewModel
  28. @Inject AddEventViewModel() { eventDateTime = LocalDateTime.now(); } void addEvent() {

    Event event = new Event(0, eventName, eventDescription, eventDateTime); eventRepository.addEvent(event).observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new CompletableObserver() { @Override public void onSubscribe(Disposable d) { } @Override public void onComplete() { Log.d(TAG, ”onComplete - successfully added event"); } @Override public void onError(Throwable e) { Log.d(TAG, e, "onError - add:"); } }); }
  29. @Inject AddEventViewModel() { eventDateTime = LocalDateTime.now(); } void addEvent() {

    Event event = new Event(0, eventName, eventDescription, eventDateTime); eventRepository.addEvent(event).observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new CompletableObserver() { @Override public void onSubscribe(Disposable d) { } @Override public void onComplete() { Log.d(TAG, ”onComplete - successfully added event"); } @Override public void onError(Throwable e) { Log.d(TAG, e, "onError - add:"); } }); }
  30. View

  31. View ViewModel Model Retrofit API Room Database Web Service SQLite

    MVVM
  32. View AddEventFragment, EventListFragment MVVM - View

  33. public class AddEventFragment extends Fragment { private Button buttonAddEvent; private

    AddEventViewModel addEventViewModel; @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_add_event, container, false); //.. setupClickListeners(); setupViewModel(); return view; } private void setupViewModel() { addEventViewModel = ViewModelProviders.of(this).get(AddEventViewModel.class); } private void setupClickListeners() { buttonAddEvent.setOnClickListener(v -> { addEventViewModel.addEvent(); getActivity().finish(); }); } }
  34. public class AddEventFragment extends Fragment { private Button buttonAddEvent; private

    AddEventViewModel addEventViewModel; @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_add_event, container, false); //.. setupClickListeners(); setupViewModel(); return view; } private void setupViewModel() { addEventViewModel = ViewModelProviders.of(this).get(AddEventViewModel.class); } private void setupClickListeners() { buttonAddEvent.setOnClickListener(v -> { addEventViewModel.addEvent(); getActivity().finish(); }); } }
  35. public class AddEventFragment extends Fragment { private Button buttonAddEvent; private

    AddEventViewModel addEventViewModel; @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_add_event, container, false); //.. setupClickListeners(); setupViewModel(); return view; } private void setupViewModel() { addEventViewModel = ViewModelProviders.of(this).get(AddEventViewModel.class); } private void setupClickListeners() { buttonAddEvent.setOnClickListener(v -> { addEventViewModel.addEvent(); getActivity().finish(); }); } }
  36. public class AddEventFragment extends Fragment { private Button buttonAddEvent; private

    AddEventViewModel addEventViewModel; @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_add_event, container, false); //.. setupClickListeners(); setupViewModel(); return view; } private void setupViewModel() { addEventViewModel = ViewModelProviders.of(this).get(AddEventViewModel.class); } private void setupClickListeners() { buttonAddEvent.setOnClickListener(v -> { addEventViewModel.addEvent(); getActivity().finish(); }); } }
  37. Demo

  38. None
  39. Let’s chat about tests…

  40. None
  41. View ViewModel Model Retrofit API Room Database Web Service SQLite

    JUnit tests MVVM
  42. View ViewModel Model Retrofit API Room Database Web Service SQLite

    Instrumentation Tests MVVM
  43. Abstract out Android Logic as much as possible. Write plenty

    of JUnit Tests, not as many UI Tests
  44. None
  45. @riggaroo JUnit Tests - Run on JVM - No Android

    Dependencies - Use Mockito - mock out everything but current class under test
  46. @riggaroo Instrumentation Tests - Two Types: - Android Unit Test

    - Use Android dependencies - No UI - UI Test - Use Espresso - Physical clicks on device - Hardly any mocking (maybe HTTP responses)
  47. Testing Database Queries

  48. View ViewModel Model Retrofit API Room Database Web Service SQLite

    Testing Room
  49. @RunWith(AndroidJUnit4.class) public class EventDaoTest { EventDao eventDao; EventDatabase eventDatabase; @Before

    public void setup() { eventDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), EventDatabase.class).build(); eventDao = eventDatabase.eventDao(); } @Test public void addEvent_SuccessfullyAddsEvent() throws InterruptedException { Event event = generateEventTestData(0, "Wedding"); eventDao.addEvent(event); List<Event> eventRetrieved = getValue(eventDao.getEvents(LocalDateTime.now())); assertEquals(event.getName(), eventRetrieved.get(0).getName()); } }
  50. @RunWith(AndroidJUnit4.class) public class EventDaoTest { EventDao eventDao; EventDatabase eventDatabase; @Before

    public void setup() { eventDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), EventDatabase.class).build(); eventDao = eventDatabase.eventDao(); } @Test public void addEvent_SuccessfullyAddsEvent() throws InterruptedException { Event event = generateEventTestData(0, "Wedding"); eventDao.addEvent(event); List<Event> eventRetrieved = getValue(eventDao.getEvents(LocalDateTime.now())); assertEquals(event.getName(), eventRetrieved.get(0).getName()); } }
  51. @RunWith(AndroidJUnit4.class) public class EventDaoTest { EventDao eventDao; EventDatabase eventDatabase; @Before

    public void setup() { eventDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), EventDatabase.class).build(); eventDao = eventDatabase.eventDao(); } @Test public void addEvent_SuccessfullyAddsEvent() throws InterruptedException { Event event = generateEventTestData(0, "Wedding"); eventDao.addEvent(event); List<Event> eventRetrieved = getValue(eventDao.getEvents(LocalDateTime.now())); assertEquals(event.getName(), eventRetrieved.get(0).getName()); } }
  52. @RunWith(AndroidJUnit4.class) public class EventDaoTest { EventDao eventDao; EventDatabase eventDatabase; @Before

    public void setup() { eventDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), EventDatabase.class).build(); eventDao = eventDatabase.eventDao(); } @Test public void addEvent_SuccessfullyAddsEvent() throws InterruptedException { Event event = generateEventTestData(0, "Wedding"); eventDao.addEvent(event); List<Event> eventRetrieved = getValue(eventDao.getEvents(LocalDateTime.now())); assertEquals(event.getName(), eventRetrieved.get(0).getName()); } }
  53. Testing Models/ Repositories

  54. View ViewModel Model Retrofit API Room Database Web Service SQLite

    Model Tests
  55. public class EventRepositoryImplTest { @Mock private EventDao eventDao; @Rule public

    InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule(); private EventRepository eventRepository; @Before public void setUp() { MockitoAnnotations.initMocks(this); eventRepository = new EventRepositoryImpl(eventDao); } @Test public void addEvent_TriggersDbAdd(){ Event event = FakeEventDataGenerator.getFakeEvent(); eventRepository.addEvent(event).test().onComplete(); verify(eventDao).addEvent(event); } }
  56. public class EventRepositoryImplTest { @Mock private EventDao eventDao; @Rule public

    InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule(); private EventRepository eventRepository; @Before public void setUp() { MockitoAnnotations.initMocks(this); eventRepository = new EventRepositoryImpl(eventDao); } @Test public void addEvent_TriggersDbAdd(){ Event event = FakeEventDataGenerator.getFakeEvent(); eventRepository.addEvent(event).test().onComplete(); verify(eventDao).addEvent(event); } }
  57. public class EventRepositoryImplTest { @Mock private EventDao eventDao; @Rule public

    InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule(); private EventRepository eventRepository; @Before public void setUp() { MockitoAnnotations.initMocks(this); eventRepository = new EventRepositoryImpl(eventDao); } @Test public void addEvent_TriggersDbAdd(){ Event event = FakeEventDataGenerator.getFakeEvent(); eventRepository.addEvent(event).test().onComplete(); verify(eventDao).addEvent(event); } }
  58. Testing ViewModels

  59. View ViewModel Model Retrofit API Room Database Web Service SQLite

    View Model Tests
  60. public class AddEventViewModelTest { AddEventViewModel addEventViewModel; @Mock EventRepository eventRepository; @Rule

    public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule(); @Before public void setup() { MockitoAnnotations.initMocks(this); addEventViewModel = new AddEventViewModel(); addEventViewModel.eventRepository = eventRepository; } @Test public void addEvent() { when(eventRepository.addEvent(any())).thenReturn(Completable.complete()); addEventViewModel.addEvent(); verify(eventRepository).addEvent(any()); }
  61. public class AddEventViewModelTest { AddEventViewModel addEventViewModel; @Mock EventRepository eventRepository; @Rule

    public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule(); @Before public void setup() { MockitoAnnotations.initMocks(this); addEventViewModel = new AddEventViewModel(); addEventViewModel.eventRepository = eventRepository; } @Test public void addEvent() { when(eventRepository.addEvent(any())).thenReturn(Completable.complete()); addEventViewModel.addEvent(); verify(eventRepository).addEvent(any()); }
  62. public class AddEventViewModelTest { AddEventViewModel addEventViewModel; @Mock EventRepository eventRepository; @Rule

    public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule(); @Before public void setup() { MockitoAnnotations.initMocks(this); addEventViewModel = new AddEventViewModel(); addEventViewModel.eventRepository = eventRepository; } @Test public void addEvent() { when(eventRepository.addEvent(any())).thenReturn(Completable.complete()); addEventViewModel.addEvent(); verify(eventRepository).addEvent(any()); }
  63. View Tests ☕

  64. View ViewModel Model Retrofit API Room Database Web Service SQLite

    View Tests
  65. Espresso Basic Formula

  66. @RunWith(AndroidJUnit4.class) public class AddEventFragmentTest { @Rule public ActivityTestRule<AddEventActivity> activityTestRule =

    new ActivityTestRule<>(AddEventActivity.class, true, true); private AddEventViewModel addEventViewModel; @Before public void setup(){ addEventViewModel = mock(AddEventViewModel.class); //.. } @Test public void addEvent(){ when(addEventViewModel.getEventDateTime()).thenReturn(LocalDateTime.now()); onView(withId(R.id.edit_text_title)).perform(typeText("Christmas Day"), closeSoftKeyboard()); onView(withId(R.id.button_set_date)).perform(click()); onView(withText("OK")).perform(scrollTo(), click()); onView(withId(R.id.button_add)).perform(click());
  67. public ActivityTestRule<AddEventActivity> activityTestRule = new ActivityTestRule<>(AddEventActivity.class, true, true); private AddEventViewModel

    addEventViewModel; @Before public void setup(){ addEventViewModel = mock(AddEventViewModel.class); //.. } @Test public void addEvent(){ when(addEventViewModel.getEventDateTime()).thenReturn(LocalDateTime.now()); onView(withId(R.id.edit_text_title)).perform(typeText("Christmas Day"), closeSoftKeyboard()); onView(withId(R.id.button_set_date)).perform(click()); onView(withText("OK")).perform(scrollTo(), click()); onView(withId(R.id.button_add)).perform(click()); } }
  68. public ActivityTestRule<AddEventActivity> activityTestRule = new ActivityTestRule<>(AddEventActivity.class, true, true); private AddEventViewModel

    addEventViewModel; @Before public void setup(){ addEventViewModel = mock(AddEventViewModel.class); //.. } @Test public void addEvent(){ when(addEventViewModel.getEventDateTime()).thenReturn(LocalDateTime.now()); onView(withId(R.id.edit_text_title)).perform(typeText("Christmas Day"), closeSoftKeyboard()); onView(withId(R.id.button_set_date)).perform(click()); onView(withText("OK")).perform(scrollTo(), click()); onView(withId(R.id.button_add)).perform(click()); } }
  69. public ActivityTestRule<AddEventActivity> activityTestRule = new ActivityTestRule<>(AddEventActivity.class, true, true); private AddEventViewModel

    addEventViewModel; @Before public void setup(){ addEventViewModel = mock(AddEventViewModel.class); //.. } @Test public void addEvent(){ when(addEventViewModel.getEventDateTime()).thenReturn(LocalDateTime.now()); onView(withId(R.id.edit_text_title)).perform(typeText("Christmas Day"), closeSoftKeyboard()); onView(withId(R.id.button_set_date)).perform(click()); onView(withText("OK")).perform(scrollTo(), click()); onView(withId(R.id.button_add)).perform(click()); } }
  70. Running UI Tests

  71. None
  72. What about Android classes or static methods?

  73. JUnit tests View ViewModel Repository ResourceProvider SharedPrefProvider MVVM

  74. Android Unit tests View ViewModel Repository ResourceProvider SharedPrefProvider MVVM

  75. public class ResourceProvider { private final Context context; public ResourceProvider(Context

    context){ this.context = context; } public String getString(Integer resId){ return context.getString(resId); } }
  76. None
  77. Summary Encourages clean code ( UI Tests using Espresso Less

    memory leaks and bugs Simplifies code
  78. @riggaroo Thank you! developer.android.com/arch Rebecca Franks @riggaroo