Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Rebecca Franks

March 27, 2018
Tweet

More Decks by Rebecca Franks

Other Decks in Programming

Transcript

  1. What are the Android Architecture Components? Powerful APIs that are

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

    Lifecycle * - Paging Library * - …
  3. 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; } }
  4. 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); }
  5. Creating a Database @Database(entities = {Event.class}, version = 1) public

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

    context) { return Room.databaseBuilder(context, EventDatabase.class, “event.db").build(); }
  7. 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)); } }
  8. 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.
  9. What is a ViewModel? - The VM in MVVM architecture

    - Store & manage UI-related data - Data survives config changes - No leaks
  10. 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
  11. @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:"); } }); }
  12. @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:"); } }); }
  13. 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(); }); } }
  14. 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(); }); } }
  15. 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(); }); } }
  16. 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(); }); } }
  17. @riggaroo JUnit Tests - Run on JVM - No Android

    Dependencies - Use Mockito - mock out everything but current class under test
  18. @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)
  19. @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()); } }
  20. @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()); } }
  21. @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()); } }
  22. @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()); } }
  23. 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); } }
  24. 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); } }
  25. 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); } }
  26. 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()); }
  27. 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()); }
  28. 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()); }
  29. @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());
  30. 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()); } }
  31. 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()); } }
  32. 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()); } }
  33. public class ResourceProvider { private final Context context; public ResourceProvider(Context

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

    memory leaks and bugs Simplifies code