88

Testable android apps

Embed Size (px)

Citation preview

Writing Testable Apps

Tested apps are better apps, but building them is tough. They have seams. DI gives you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

Tested apps are better apps, but building them is tough. They have seams. DI gives you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

Why tests?

Why tests?

Why are we here?

“the goal of software delivery is to sustainably minimize the lead time to business

impact”

Yes, but why tests?

–Steve Freeman and Nat Pryce, authors of Growing Object Oriented Software Guided by Tests

“for a class to be easy to unit-test, the class must…be loosely coupled and highly cohesive

—in other words, well-designed.”

“We invest in this huge testing framework…

engineers here have the power to try out an idea

and ship it to maybe 10,000 people or 100,000

people.”

Tested apps are better apps, but building them is tough. They have seams. DI gives you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

Tested apps are better apps, but building them is tough. They have seams. DI gives you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

–Michael Feathers, Working Effectively with Legacy Code

“One of the things that nearly everyone notices when they try to write tests for existing code is

just how poorly suited code is to testing.”

public class PresenterFragmentImpl extends Fragment implements Presenter, UpdatableView.UserActionListener, LoaderManager.LoaderCallbacks<Cursor> {

@Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { Loader<Cursor> cursorLoader = createLoader(id, args); mLoaderIdlingResource.onLoaderStarted(cursorLoader); return cursorLoader; }

@Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { processData(loader, data); mLoaderIdlingResource.onLoaderFinished(loader); } }

public class PresenterFragmentImpl extends Fragment implements Presenter, UpdatableView.UserActionListener, LoaderManager.LoaderCallbacks<Cursor> {

@Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { Loader<Cursor> cursorLoader = createLoader(id, args); mLoaderIdlingResource.onLoaderStarted(cursorLoader); return cursorLoader; }

@Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { processData(loader, data); mLoaderIdlingResource.onLoaderFinished(loader); } }

What makes code testable?

@Override public void onSharedPreferenceChanged(SharedPreferences sharedPrefs,

String key) {

if (SettingsUtils.PREF_SYNC_CALENDAR.equals(key)) { Intent intent; if (SettingsUtils.shouldSyncCalendar(getActivity())) { // Add all calendar entries intent = new Intent(ACTION_UPDATE_ALL_SESSIONS_CALENDAR); } else { // Remove all calendar entries intent = new Intent(ACTION_CLEAR_ALL_SESSIONS_CALENDAR); }

intent.setClass(getActivity(), SessionCalendarService.class); getActivity().startService(intent); } }

@Override public void onSharedPreferenceChanged(SharedPreferences sharedPrefs,

String key) {

if (SettingsUtils.PREF_SYNC_CALENDAR.equals(key)) { Intent intent; if (SettingsUtils.shouldSyncCalendar(getActivity())) { // Add all calendar entries intent = new Intent(ACTION_UPDATE_ALL_SESSIONS_CALENDAR); } else { // Remove all calendar entries intent = new Intent(ACTION_CLEAR_ALL_SESSIONS_CALENDAR); }

intent.setClass(getActivity(), SessionCalendarService.class); getActivity().startService(intent); } }

@Override public void onSharedPreferenceChanged(SharedPreferences sharedPrefs,

String key) {

if (SettingsUtils.PREF_SYNC_CALENDAR.equals(key)) { Intent intent; if (SettingsUtils.shouldSyncCalendar(getActivity())) { // Add all calendar entries intent = new Intent(ACTION_UPDATE_ALL_SESSIONS_CALENDAR); } else { // Remove all calendar entries intent = new Intent(ACTION_CLEAR_ALL_SESSIONS_CALENDAR); }

intent.setClass(getActivity(), SessionCalendarService.class); getActivity().startService(intent); } }

@Test public void onSPChangedRemovesSessions() throws Exception { // Arrange

//Act mSettingsFragment.onSPChanged(mMockSharedPreferences, PREF_SYNC_CALENDAR);

//Assert

}

@Test public void onSPChangedRemovesSessions() throws Exception { // Arrange

//Act mSettingsFragment.onSPChanged(mMockSharedPreferences, PREF_SYNC_CALENDAR);

//Assert

}

@Override public void onSharedPreferenceChanged(SharedPreferences sharedPrefs,

String key) {

if (SettingsUtils.PREF_SYNC_CALENDAR.equals(key)) { Intent intent; if (SettingsUtils.shouldSyncCalendar(getActivity())) { // Add all calendar entries intent = new Intent(ACTION_UPDATE_ALL_SESSIONS_CALENDAR); } else { // Remove all calendar entries intent = new Intent(ACTION_CLEAR_ALL_SESSIONS_CALENDAR); }

intent.setClass(getActivity(), SessionCalendarService.class); getActivity().startService(intent); } }

@Test public void onSPChangedRemovesSessions() throws Exception { // Arrange

//Act mSettingsFragment.onSPChanged(mMockSharedPreferences, PREF_SYNC_CALENDAR);

//Assert

}

@Override public void onSharedPreferenceChanged(SharedPreferences sharedPrefs,

String key) {

if (SettingsUtils.PREF_SYNC_CALENDAR.equals(key)) { Intent intent; if (SettingsUtils.shouldSyncCalendar(getActivity())) { // Add all calendar entries intent = new Intent(ACTION_UPDATE_ALL_SESSIONS_CALENDAR); } else { // Remove all calendar entries intent = new Intent(ACTION_CLEAR_ALL_SESSIONS_CALENDAR); }

intent.setClass(getActivity(), SessionCalendarService.class); getActivity().startService(intent); } }

Tested apps are better apps, but building them is tough. They have seams. DI gives you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

Tested apps are better apps, but building them is tough. They have seams. DI gives you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

–Michael Feathers, author of Working Effectively with Legacy Code

“A seam is a place where you can alter behavior in your program without editing in that

place.”

Without seams, it’s often difficult to arrange and/or

assert

class CalendarUpdatingOnSharedPreferenceChangedListener {

void onPreferenceChanged(CalendarPreferences calendarPreferences, String key) {

if (SettingsUtils.PREF_SYNC_CALENDAR.equals(key)) { if (calendarPreferences.shouldSyncCalendar()) { mSessUpdaterLauncher.launchAddAllSessionsUpdater(); } else { mSessUpdaterLauncher.launchClearAllSessionsUpdate(); } } } }

class CalendarUpdatingOnSharedPreferenceChangedListener {

void onPreferenceChanged(CalendarPreferences calendarPreferences, String key) {

if (SettingsUtils.PREF_SYNC_CALENDAR.equals(key)) { if (calendarPreferences.shouldSyncCalendar()) { mSessUpdaterLauncher.launchAddAllSessionsUpdater(); } else { mSessUpdaterLauncher.launchClearAllSessionsUpdate(); } } } }

class CalendarUpdatingOnSharedPreferenceChangedListener {

void onPreferenceChanged(CalendarPreferences calendarPreferences, String key) {

if (SettingsUtils.PREF_SYNC_CALENDAR.equals(key)) { if (calendarPreferences.shouldSyncCalendar()) { mSessUpdaterLauncher.launchAddAllSessionsUpdater(); } else { mSessUpdaterLauncher.launchClearAllSessionsUpdate(); } } } }

class CalendarUpdatingOnSharedPreferenceChangedListener {

void onPreferenceChanged(CalendarPreferences calendarPreferences, String key) {

if (SettingsUtils.PREF_SYNC_CALENDAR.equals(key)) { if (calendarPreferences.shouldSyncCalendar()) { mSessUpdaterLauncher.launchAddAllSessionsUpdater(); } else { mSessUpdaterLauncher.launchClearAllSessionsUpdate(); } } } }

@Test public void onPreferenceChangedClearedCalendar() throws Exception {

// Arrange CUOSPCListener listener = new CUOSPCListener(mSessionUpdateLauncher);

final CalendarPreferences calendarPreferences = mock(CalendarPreferences.class); when(calendarPreferences.shouldSyncCalendar()).thenReturn(false);

// Act listener.onPreferenceChanged(calendarPreferences, SettingsUtils.PREF_SYNC_CALENDAR);

// Assert verify(mSessionUpdateLauncher).launchClearAllSessionsUpdate(); }

@Test public void onPreferenceChangedClearedCalendar() throws Exception {

// Arrange CUOSPCListener listener = new CUOSPCListener(mSessionUpdateLauncher);

final CalendarPreferences calendarPreferences = mock(CalendarPreferences.class); when(calendarPreferences.shouldSyncCalendar()).thenReturn(false);

// Act listener.onPreferenceChanged(calendarPreferences, SettingsUtils.PREF_SYNC_CALENDAR);

// Assert verify(mSessionUpdateLauncher).launchClearAllSessionsUpdate(); }

@Test public void onPreferenceChangedClearedCalendar() throws Exception {

// Arrange CUOSPCListener listener = new CUOSPCListener(mSessionUpdateLauncher);

final CalendarPreferences calendarPreferences = mock(CalendarPreferences.class); when(calendarPreferences.shouldSyncCalendar()).thenReturn(false);

// Act listener.onPreferenceChanged(calendarPreferences, SettingsUtils.PREF_SYNC_CALENDAR);

// Assert verify(mSessionUpdateLauncher).launchClearAllSessionsUpdate(); }

class CalendarUpdatingOnSharedPreferenceChangedListener {

void onPreferenceChanged(CalendarPreferences calendarPreferences, String key) {

if (SettingsUtils.PREF_SYNC_CALENDAR.equals(key)) { if (calendarPreferences.shouldSyncCalendar()) { mSessUpdaterLauncher.launchAddAllSessionsUpdater(); } else { mSessUpdaterLauncher.launchClearAllSessionsUpdate(); } } } }

Tested apps are better apps, but building them is tough. They have seams. DI gives you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

Tested apps are better apps, but building them is tough. They have seams. DI gives

you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

Object Seams

–Michael Feathers

“The fundamental thing to recognize is that when we look at a call in an object-oriented

program, it does not define which method will actually be executed.”

DI != Dagger

The code that needs dependencies is not

responsible for getting them

Tested apps are better apps, but building them is tough. They have seams. DI gives

you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

Tested apps are better apps, but building them is tough. They have seams. DI gives

you Object Seams, which is why MVP helps testability. Build Variants give you Link Seams, but don’t overuse

them.

private void setupCards(CollectionView.Inventory inventory) { if (SettingsUtils.isAttendeeAtVenue(getContext())) { if (!hasAnsweredConfMessageCardsPrompt(getContext())) { inventoryGroup = new InventoryGroup(GROUP_ID_MESSAGE_CARDS);

MessageData conferenceMessageOptIn = MessageCardHelper .getConferenceOptInMessageData(getContext()); inventoryGroup.addItemWithTag(conferenceMessageOptIn); inventoryGroup.setDisplayCols(1); inventory.addGroup(inventoryGroup); } // ... } }

private void setupCards(CollectionView.Inventory inventory) { if (SettingsUtils.isAttendeeAtVenue(getContext())) { if (!hasAnsweredConfMessageCardsPrompt(getContext())) { inventoryGroup = new InventoryGroup(GROUP_ID_MESSAGE_CARDS);

MessageData conferenceMessageOptIn = MessageCardHelper .getConferenceOptInMessageData(getContext()); inventoryGroup.addItemWithTag(conferenceMessageOptIn); inventoryGroup.setDisplayCols(1); inventory.addGroup(inventoryGroup); } // ... } }

private void setupCards(CollectionView.Inventory inventory) { if (SettingsUtils.isAttendeeAtVenue(getContext())) { if (!hasAnsweredConfMessageCardsPrompt(getContext())) { inventoryGroup = new InventoryGroup(GROUP_ID_MESSAGE_CARDS);

MessageData conferenceMessageOptIn = MessageCardHelper .getConferenceOptInMessageData(getContext()); inventoryGroup.addItemWithTag(conferenceMessageOptIn); inventoryGroup.setDisplayCols(1); inventory.addGroup(inventoryGroup); } // ... } }

class Presenter {

public void presentCards() {

if (mIsAttendeeAtVenue) {

if (!mMsgSettings.hasAnsweredMessagePrompt()) {

mExploreView.addMessageOptInCard();

} // Stuff } } }

class Presenter {

public void presentCards() {

if (mIsAttendeeAtVenue) {

if (!mMsgSettings.hasAnsweredMessagePrompt()) {

mExploreView.addMessageOptInCard();

} // Stuff } } }

class Presenter {

public void presentCards() {

if (mIsAttendeeAtVenue) {

if (!mMsgSettings.hasAnsweredMessagePrompt()) {

mExploreView.addMessageOptInCard();

} // Stuff } } }

class Presenter {

public void presentCards() {

if (mIsAttendeeAtVenue) {

if (!mMsgSettings.hasAnsweredMessagePrompt()) {

mExploreView.addMessageOptInCard();

} // Stuff } } }

Tested apps are better apps, but building them is tough. They have seams. DI gives

you Object Seams, which is why MVP helps testability. Build Variants give you Link Seams, but don’t overuse

them.

Tested apps are better apps, but building them is tough. They have seams. DI gives you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

–Michael Feathers

“[code] contains calls to code in other files. Linkers…resolve each of the calls so that you

can have a complete program at runtime…you can usually exploit [this] to substitute pieces of

your program”

Use Link Seams for Espresso Tests

public class PresenterFragmentImpl extends Fragment implements Presenter, UpdatableView.UserActionListener, LoaderManager.LoaderCallbacks<Cursor> {

@Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { Loader<Cursor> cursorLoader = createLoader(id, args); mLoaderIdlingResource.onLoaderStarted(cursorLoader); return cursorLoader; }

@Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { processData(loader, data); mLoaderIdlingResource.onLoaderFinished(loader); } }

public class PresenterFragmentImpl extends Fragment implements Presenter, UpdatableView.UserActionListener, LoaderManager.LoaderCallbacks<Cursor> {

@Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { Loader<Cursor> cursorLoader = createLoader(id, args); mLoaderIdlingResource.onLoaderStarted(cursorLoader); return cursorLoader; }

@Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { processData(loader, data); mLoaderIdlingResource.onLoaderFinished(loader); } }

public PresenterFragmentImpl addPresenterFragment(int uVResId, Model model, QueryEnum[] queries,

UserActionEnum[] actions){ //...

if (presenter == null) { //Create, set up and add the presenter. presenter = new PresenterFragmentImpl(); //... } else {

//... } return presenter; }

public PresenterFragmentImpl addPresenterFragment(int uVResId, Model model, QueryEnum[] queries,

UserActionEnum[] actions){ //...

if (presenter == null) { //Create, set up and add the presenter. presenter = new PresenterFragmentImpl(); //... } else {

//... } return presenter; }

flavorDimensions 'datasource', 'features' productFlavors { mock { dimension 'datasource' }

prod { dimension 'datasource' }

free { dimension 'features' } }

Tested apps are better apps, but building them is tough. They have seams. DI gives you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

Tested apps are better apps, but building them is tough. They have seams. DI gives you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

More complicated object graphs can lead to…

Use Link Seams to swap out factories so you can

use object seams

Use Link Seams to swap out factories so you can

use object seams

public class FragFactory { public PresenterFragmentImpl make() { return new PresenterFragmentImpl(); } }

public class FragFactory { public PresenterFragmentImpl make() { return new MockPresenterFragmentImpl(); } }

public class FragFactory { public PresenterFragmentImpl make() { return new PresenterFragmentImpl(); } }

public class FragFactory { public PresenterFragmentImpl make() { return new MockPresenterFragmentImpl(); } }

public class FragFactory { public PresenterFragmentImpl make() { return new PresenterFragmentImpl(); } }

public class FragFactory { public PresenterFragmentImpl make() { return new MockPresenterFragmentImpl(); } }

public PresenterFragmentImpl addPresenterFragment(int uVResId, Model model, QueryEnum[] queries,

UserActionEnum[] actions){ //...

if (presenter == null) { //Create, set up and add the presenter. presenter = new PresenterFragmentImpl(); // 1 seam //... } else {

//... } return presenter; }

public PresenterFragmentImpl addPresenterFragment(int uVResId, Model model, QueryEnum[] queries,

UserActionEnum[] actions){ //...

if (presenter == null) { //Create, set up and add the presenter. presenter = mFragFactory.make(); // 2 seams //... } else {

//... } return presenter; }

This second seam buys you “mock mode”

Tested apps are better apps, but building them is tough. They have seams. DI gives you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

Tested apps are better apps, but building them is tough. They have seams. DI gives you Object Seams, which is why MVP helps testability.

Build Variants give you Link Seams, but don’t overuse

them.

Writing Testable Apps

• “Microservices: Software That Fits in Your Head”

• “Mark Zuckerberg: How to Build the Future”

• Growing Object Oriented Software Guided by Tests

• Working Effectively with Legacy Code

• “Dependency Injection” by Martin Fowler

• “Android Apps with Dagger” by Jake Wharton

Sources