Share
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
Transcript
78 slides
-
What are the Android Architecture Components? Powerful APIs that are
testable " Improve app quality ✅ Address common issues Best practice guidelines % -
@riggaroo Components - Room - ViewModels - LiveData * -
Lifecycle * - Paging Library * - … -
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; } } -
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); } -
Creating a Database @Database(entities = {Event.class}, version = 1) public
abstract class EventDatabase extends RoomDatabase { public abstract EventDao eventDao(); } -
Create an instance of the DB @Provides @Singleton EventDatabase providesEventDatabase(CountdownApplication
context) { return Room.databaseBuilder(context, EventDatabase.class, “event.db").build(); } -
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)); } } -
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. -
What is a ViewModel? - The VM in MVVM architecture
- Store & manage UI-related data - Data survives config changes - No leaks -
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 -
@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:"); } }); } -
@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:"); } }); } -
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(); }); } } -
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(); }); } } -
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(); }); } } -
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(); }); } } -
View ViewModel Model Retrofit API Room Database Web Service SQLite
JUnit tests MVVM -
View ViewModel Model Retrofit API Room Database Web Service SQLite
Instrumentation Tests MVVM -
Abstract out Android Logic as much as possible. Write plenty
of JUnit Tests, not as many UI Tests -
@riggaroo JUnit Tests - Run on JVM - No Android
Dependencies - Use Mockito - mock out everything but current class under test -
@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) -
@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()); } } -
@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()); } } -
@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()); } } -
@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()); } } -
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); } } -
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); } } -
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); } } -
View ViewModel Model Retrofit API Room Database Web Service SQLite
View Model Tests -
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()); } -
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()); } -
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()); } -
@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()); -
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()); } } -
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()); } } -
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()); } } -
public class ResourceProvider { private final Context context; public ResourceProvider(Context
context){ this.context = context; } public String getString(Integer resId){ return context.getString(resId); } } -
Summary Encourages clean code ( UI Tests using Espresso Less
memory leaks and bugs Simplifies code