Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

If Android Tests Could Talk

58 views

Published on

TDD as architecture feedback

Published in: Technology
  • Be the first to comment

  • Be the first to like this

If Android Tests Could Talk

  1. 1. If Android Tests Could Talk Matt Dupree
  2. 2. Their pace has quickened
  3. 3. This code is garbage
  4. 4. –Steve Freeman and Nat Pryce, 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.”
  5. 5. –Steve Freeman and Nat Pryce, 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.”
  6. 6. –Steve Freeman and Nat Pryce, 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.”
  7. 7. Activities tend to have tight coupling and poor cohesion
  8. 8. Application components (activities, services, providers, receivers) are interfaces for your application to interact with the operating system; don’t take them as a recommendation of the facilities you should architect your entire application around. —Chet Haase, Developing for Android VII
  9. 9. Activities tend to have tight coupling and poor cohesion
  10. 10. @Override
 public void onRequestPermissionsResult(final int requestCode,
 @NonNull final String[] permissions,
 @NonNull final int[] grantResults) {
 
 if (requestCode != REQUEST_LOCATION_PERMISSION) {
 return;
 }
 
 if (permissions.length == 1 &&
 LOCATION_PERMISSION.equals(permissions[0]) &&
 grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 // Permission has been granted.
 if (mMapFragment != null) {
 mMapFragment.setMyLocationEnabled(true);
 }
 } else {
 // Permission was denied. Display error message.
 Toast.makeText(this, R.string.map_permission_denied,
 Toast.LENGTH_SHORT).show();
 }
 super.onRequestPermissionsResult(requestCode, permissions,
 grantResults);
 }
  11. 11. @Override
 public void onRequestPermissionsResult(final int requestCode,
 @NonNull final String[] permissions,
 @NonNull final int[] grantResults) {
 
 if (requestCode != REQUEST_LOCATION_PERMISSION) {
 return;
 }
 
 if (permissions.length == 1 &&
 LOCATION_PERMISSION.equals(permissions[0]) &&
 grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 // Permission has been granted.
 if (mMapFragment != null) {
 mMapFragment.setMyLocationEnabled(true);
 }
 } else {
 // Permission was denied. Display error message.
 Toast.makeText(this, R.string.map_permission_denied,
 Toast.LENGTH_SHORT).show();
 }
 super.onRequestPermissionsResult(requestCode, permissions,
 grantResults);
 }
  12. 12. @Override
 public void onRequestPermissionsResult(final int requestCode,
 @NonNull final String[] permissions,
 @NonNull final int[] grantResults) {
 
 if (requestCode != REQUEST_LOCATION_PERMISSION) {
 return;
 }
 
 if (permissions.length == 1 &&
 LOCATION_PERMISSION.equals(permissions[0]) &&
 grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 // Permission has been granted.
 if (mMapFragment != null) {
 mMapFragment.setMyLocationEnabled(true);
 }
 } else {
 // Permission was denied. Display error message.
 Toast.makeText(this, R.string.map_permission_denied,
 Toast.LENGTH_SHORT).show();
 }
 super.onRequestPermissionsResult(requestCode, permissions,
 grantResults);
 }
  13. 13. @Override
 public void onRequestPermissionsResult(final int requestCode,
 @NonNull final String[] permissions,
 @NonNull final int[] grantResults) {
 
 if (requestCode != REQUEST_LOCATION_PERMISSION) {
 return;
 }
 
 if (permissions.length == 1 &&
 LOCATION_PERMISSION.equals(permissions[0]) &&
 grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 // Permission has been granted.
 if (mMapFragment != null) {
 mMapFragment.setMyLocationEnabled(true);
 }
 } else {
 // Permission was denied. Display error message.
 Toast.makeText(this, R.string.map_permission_denied,
 Toast.LENGTH_SHORT).show();
 }
 super.onRequestPermissionsResult(requestCode, permissions,
 grantResults);
 }
  14. 14. @Override
 public void onRequestPermissionsResult(final int requestCode,
 @NonNull final String[] permissions,
 @NonNull final int[] grantResults) {
 
 if (requestCode != REQUEST_LOCATION_PERMISSION) {
 return;
 }
 
 if (permissions.length == 1 &&
 LOCATION_PERMISSION.equals(permissions[0]) &&
 grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 // Permission has been granted.
 if (mMapFragment != null) {
 mMapFragment.setMyLocationEnabled(true);
 }
 } else {
 // Permission was denied. Display error message.
 Toast.makeText(this, R.string.map_permission_denied,
 Toast.LENGTH_SHORT).show();
 }
 super.onRequestPermissionsResult(requestCode, permissions,
 grantResults);
 }
  15. 15. @Test
 public void showsToastIfPermissionIsRejected()
 throws Exception {
 MapActivity mapActivity = new MapActivity();
 
 mapActivity.onRequestPermissionsResult(
 MapActivity.REQUEST_LOCATION_PERMISSION,
 new String[]{MapActivity.LOCATION_PERMISSION}, new int[]{
 PackageManager.PERMISSION_DENIED});
 
 assertToastDisplayed();
 }
  16. 16. @Test
 public void showsToastIfPermissionIsRejected()
 throws Exception {
 MapActivity mapActivity = new MapActivity();
 
 mapActivity.onRequestPermissionsResult(
 MapActivity.REQUEST_LOCATION_PERMISSION,
 new String[]{MapActivity.LOCATION_PERMISSION}, new int[]{
 PackageManager.PERMISSION_DENIED});
 
 assertToastDisplayed();
 }
  17. 17. @Test
 public void showsToastIfPermissionIsRejected()
 throws Exception {
 MapActivity mapActivity = new MapActivity();
 
 mapActivity.onRequestPermissionsResult(
 MapActivity.REQUEST_LOCATION_PERMISSION,
 new String[]{MapActivity.LOCATION_PERMISSION}, new int[]{
 PackageManager.PERMISSION_DENIED});
 
 assertToastDisplayed();
 }
  18. 18. @Test
 public void showsToastIfPermissionIsRejected()
 throws Exception {
 MapActivity mapActivity = new MapActivity();
 
 mapActivity.onRequestPermissionsResult(
 MapActivity.REQUEST_LOCATION_PERMISSION,
 new String[]{MapActivity.LOCATION_PERMISSION}, new int[]{
 PackageManager.PERMISSION_DENIED});
 
 assertToastDisplayed();
 }
  19. 19. private void assertToastDisplayed() {
 // It can't be done, sir 
 }
  20. 20. @Override
 public void onRequestPermissionsResult(final int requestCode,
 @NonNull final String[] permissions,
 @NonNull final int[] grantResults) {
 
 if (requestCode != REQUEST_LOCATION_PERMISSION) {
 return;
 }
 
 if (permissions.length == 1 &&
 LOCATION_PERMISSION.equals(permissions[0]) &&
 grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 // Permission has been granted.
 if (mMapFragment != null) {
 mMapFragment.setMyLocationEnabled(true);
 }
 } else {
 // Permission was denied. Display error message.
 Toast.makeText(this, R.string.map_permission_denied,
 Toast.LENGTH_SHORT).show();
 }
 super.onRequestPermissionsResult(requestCode, permissions,
 grantResults);
 }
  21. 21. MapActivity is tightly coupled with MapFragment and Toast
  22. 22. static class OnPermissionResultListener { 
 private final PermittedView mPermittedView;
 
 void onPermissionResult(final int requestCode,
 final String[] permissions, final int[] grantResults) {}
 
 interface PermittedView { 
 void displayPermissionDenied(); void displayPermittedView();
 }
 }
  23. 23. static class OnPermissionResultListener { 
 private final PermittedView mPermittedView;
 
 void onPermissionResult(final int requestCode,
 final String[] permissions, final int[] grantResults) {}
 
 interface PermittedView { 
 void displayPermissionDenied(); void displayPermittedView();
 }
 }
  24. 24. static class OnPermissionResultListener { 
 private final PermittedView mPermittedView;
 
 void onPermissionResult(final int requestCode,
 final String[] permissions, final int[] grantResults) {}
 
 interface PermittedView { 
 void displayPermissionDenied(); void displayPermittedView();
 }
 }
  25. 25. void onPermissionResult(final int requestCode,
 final String[] permissions, final int[] grantResults) {
 if (requestCode != MapActivity.REQUEST_LOCATION_PERMISSION) {
 return;
 }
 
 if (permissions.length == 1 &&
 MapActivity.LOCATION_PERMISSION.equals(permissions[0]) &&
 grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 // Permission has been granted.
 mPermittedView.displayPermittedView();
 
 } else {
 // Permission was denied. Display error message.
 mPermittedView.displayPermissionDenied();
 }
 }
  26. 26. void onPermissionResult(final int requestCode,
 final String[] permissions, final int[] grantResults) {
 if (requestCode != MapActivity.REQUEST_LOCATION_PERMISSION) {
 return;
 }
 
 if (permissions.length == 1 &&
 MapActivity.LOCATION_PERMISSION.equals(permissions[0]) &&
 grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 // Permission has been granted.
 mPermittedView.displayPermittedView();
 
 } else {
 // Permission was denied. Display error message.
 mPermittedView.displayPermissionDenied();
 }
 }
  27. 27. @Test
 public void displaysErrorWhenPermissionRejected() throws Exception {
 
 OnPermissionResultListener onPermissionResultListener =
 new OnPermissionResultListener(mPermittedView);
 
 onPermissionResultListener.onPermissionResult(
 MapActivity.REQUEST_LOCATION_PERMISSION,
 new String[]{MapActivity.LOCATION_PERMISSION},
 new int[]{PackageManager.PERMISSION_DENIED});
 
 verify(mPermittedView).displayPermissionDenied();
 }
  28. 28. @Test
 public void displaysErrorWhenPermissionRejected() throws Exception {
 
 OnPermissionResultListener onPermissionResultListener =
 new OnPermissionResultListener(mPermittedView);
 
 onPermissionResultListener.onPermissionResult(
 MapActivity.REQUEST_LOCATION_PERMISSION,
 new String[]{MapActivity.LOCATION_PERMISSION},
 new int[]{PackageManager.PERMISSION_DENIED});
 
 verify(mPermittedView).displayPermissionDenied();
 }
  29. 29. @Test
 public void displaysErrorWhenPermissionRejected() throws Exception {
 
 OnPermissionResultListener onPermissionResultListener =
 new OnPermissionResultListener(mPermittedView);
 
 onPermissionResultListener.onPermissionResult(
 MapActivity.REQUEST_LOCATION_PERMISSION,
 new String[]{MapActivity.LOCATION_PERMISSION},
 new int[]{PackageManager.PERMISSION_DENIED});
 
 verify(mPermittedView).displayPermissionDenied();
 }
  30. 30. @Test
 public void displaysErrorWhenPermissionRejected() throws Exception {
 
 OnPermissionResultListener onPermissionResultListener =
 new OnPermissionResultListener(mPermittedView);
 
 onPermissionResultListener.onPermissionResult(
 MapActivity.REQUEST_LOCATION_PERMISSION,
 new String[]{MapActivity.LOCATION_PERMISSION},
 new int[]{PackageManager.PERMISSION_DENIED});
 
 verify(mPermittedView).displayPermissionDenied();
 }
  31. 31. Who cares?
  32. 32. –Edward Yourdon and Larry Constantine “what we are striving for is loosely coupled systems…in which one can study (or debug, or maintain) any one module without having to know very much about any other modules in the system”
  33. 33. @Override
 public void displayPermissionDenied() {
 Toast.makeText(MapActivity.this, R.string.map_permission_denied,
 Toast.LENGTH_SHORT).show();
 }
  34. 34. @Override
 public void displayPermissionDenied() {
 PermissionsUtils.displayConditionalPermissionDenialSnackbar(this,
 R.string.map_permission_denied, PERMISSIONS,
 REQUEST_LOCATION_PERMISSION, false);
 }
  35. 35. –Kent Beck, TDD By Example “Dependency is the key problem in software development at all scales…if dependency is the problem, duplication is the symptom.”
  36. 36. @Override
 public void onRequestPermissionsResult(final int requestCode,
 @NonNull final String[] permissions,
 @NonNull final int[] grantResults) {
 
 if (grantResults.length == APP_REQUIRED_PERMISSIONS.length &&
 grantResults[0] == PackageManager.PERMISSION_GRANTED &&
 grantResults[1] == PackageManager.PERMISSION_GRANTED) {
 // If permission granted then refresh account list so user can
 // select an account.
 clearSnackbar();
 reloadAccounts();
 refreshAccountListUI();
 } else {
 LOGI(TAG, "onRequestPermissionResult with permissions denied");
 mSnackbar = new WeakReference<>(PermissionsUtils
 .displayConditionalPermissionDenialSnackbar(
 getActivity(),
 R.string.welcome_permissions_rationale,
 APP_REQUIRED_PERMISSIONS,
 REQUEST_PERMISSION_REQUEST_CODE));
 }
 }
  37. 37. Activities tend to have tight coupling and poor cohesion
  38. 38. –Steve Freeman and Nat Pryce, 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.”
  39. 39. Activities tend to have tight coupling and poor cohesion
  40. 40. –Steve Freeman and Nat Pryce, 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.”
  41. 41. private void displaySessionData(final SessionDetailModel data) { //46 statement method inside a 900 line class 
 
 // Handle Keynote as a special case, where the user cannot remove it
 // from the schedule (it is auto added to schedule on sync)
 mShowFab = (AccountUtils.hasActiveAccount(getContext()) &&
 !data.isKeynote());
 mAddScheduleFab
 .setVisibility(mShowFab ? View.VISIBLE : View.INVISIBLE);
 
 if (!data.isKeynote()) {
 showInScheduleDeferred(data.isInSchedule());
 }
 //... 
 updateTimeBasedUi(data);
 
 
 }
  42. 42. private void displaySessionData(final SessionDetailModel data) { //46 statement method inside a 900 line class! 
 
 // Handle Keynote as a special case, where the user cannot remove it
 // from the schedule (it is auto added to schedule on sync)
 mShowFab = (AccountUtils.hasActiveAccount(getContext()) &&
 !data.isKeynote());
 mAddScheduleFab
 .setVisibility(mShowFab ? View.VISIBLE : View.INVISIBLE);
 
 if (!data.isKeynote()) {
 showInScheduleDeferred(data.isInSchedule());
 }
 //... 
 updateTimeBasedUi(data);
 
 
 }
  43. 43. private void displaySessionData(final SessionDetailModel data) { //46 statement method inside a 900 line class 
 
 // Handle Keynote as a special case, where the user cannot remove it
 // from the schedule (it is auto added to schedule on sync)
 mShowFab = (AccountUtils.hasActiveAccount(getContext()) &&
 !data.isKeynote());
 mAddScheduleFab
 .setVisibility(mShowFab ? View.VISIBLE : View.INVISIBLE);
 
 if (!data.isKeynote()) {
 showInScheduleDeferred(data.isInSchedule());
 }
 //... 
 updateTimeBasedUi(data);
 
 
 }
  44. 44. private void displaySessionData(final SessionDetailModel data) { //46 statement method inside a 900 line class 
 
 // Handle Keynote as a special case, where the user cannot remove it
 // from the schedule (it is auto added to schedule on sync)
 mShowFab = (AccountUtils.hasActiveAccount(getContext()) &&
 !data.isKeynote());
 mAddScheduleFab
 .setVisibility(mShowFab ? View.VISIBLE : View.INVISIBLE);
 
 if (!data.isKeynote()) {
 showInScheduleDeferred(data.isInSchedule());
 }
 //... 
 updateTimeBasedUi(data);
 
 
 }
  45. 45. private void displaySessionData(final SessionDetailModel data) { //46 statement method inside a 900 line class 
 
 // Handle Keynote as a special case, where the user cannot remove it
 // from the schedule (it is auto added to schedule on sync)
 mShowFab = (AccountUtils.hasActiveAccount(getContext()) &&
 !data.isKeynote());
 mAddScheduleFab
 .setVisibility(mShowFab ? View.VISIBLE : View.INVISIBLE);
 
 if (!data.isKeynote()) {
 showInScheduleDeferred(data.isInSchedule());
 }
 //... 
 updateTimeBasedUi(data);
 
 
 }
  46. 46. @Test
 public void onlyProvidesAddSessionToggleIfSessionIsNotKeynote()
 throws Exception {
 SessionDetailFragment sessionDetailFragment =
 new SessionDetailFragment();
 final SessionDetailModel sessionDetailModel =
 mock(SessionDetailModel.class);
 when(sessionDetailModel.isKeynote()).thenReturn(false);
 sessionDetailFragment.displayData(sessionDetailModel,
 SessionDetailModel.SessionDetailQueryEnum.SESSIONS);
 final View addScheduleButton =
 sessionDetailFragment.getView()
 .findViewById(R.id.add_schedule_button); assertTrue(addScheduleButton.getVisibility() == View.INVISIBLE);
 }
  47. 47. @Test
 public void onlyProvidesAddSessionToggleIfSessionIsNotKeynote()
 throws Exception {
 SessionDetailFragment sessionDetailFragment =
 new SessionDetailFragment();
 final SessionDetailModel sessionDetailModel =
 mock(SessionDetailModel.class);
 when(sessionDetailModel.isKeynote()).thenReturn(false);
 sessionDetailFragment.displayData(sessionDetailModel,
 SessionDetailModel.SessionDetailQueryEnum.SESSIONS);
 final View addScheduleButton =
 sessionDetailFragment.getView()
 .findViewById(R.id.add_schedule_button); assertTrue(addScheduleButton.getVisibility() == View.INVISIBLE);
 }
  48. 48. @Test
 public void onlyProvidesAddSessionToggleIfSessionIsNotKeynote()
 throws Exception {
 SessionDetailFragment sessionDetailFragment =
 new SessionDetailFragment();
 final SessionDetailModel sessionDetailModel =
 mock(SessionDetailModel.class);
 when(sessionDetailModel.isKeynote()).thenReturn(false);
 sessionDetailFragment.displayData(sessionDetailModel,
 SessionDetailModel.SessionDetailQueryEnum.SESSIONS);
 final View addScheduleButton =
 sessionDetailFragment.getView()
 .findViewById(R.id.add_schedule_button); assertTrue(addScheduleButton.getVisibility() == View.INVISIBLE);
 }
  49. 49. @Test
 public void onlyProvidesAddSessionToggleIfSessionIsNotKeynote()
 throws Exception {
 SessionDetailFragment sessionDetailFragment =
 new SessionDetailFragment();
 final SessionDetailModel sessionDetailModel =
 mock(SessionDetailModel.class);
 when(sessionDetailModel.isKeynote()).thenReturn(false);
 sessionDetailFragment.displayData(sessionDetailModel,
 SessionDetailModel.SessionDetailQueryEnum.SESSIONS);
 final View addScheduleButton =
 sessionDetailFragment.getView()
 .findViewById(R.id.add_schedule_button); assertTrue(addScheduleButton.getVisibility() == View.INVISIBLE);
 }
  50. 50. @Test
 public void onlyProvidesAddSessionToggleIfSessionIsNotKeynote()
 throws Exception {
 SessionDetailFragment sessionDetailFragment =
 new SessionDetailFragment();
 final SessionDetailModel sessionDetailModel =
 mock(SessionDetailModel.class);
 when(sessionDetailModel.isKeynote()).thenReturn(false);
 sessionDetailFragment.onActivityCreated(null); sessionDetailFragment.displayData(sessionDetailModel,
 SessionDetailModel.SessionDetailQueryEnum.SESSIONS);
 final View addScheduleButton =
 sessionDetailFragment.getView()
 .findViewById(R.id.add_schedule_button); assertTrue(addScheduleButton.getVisibility() == View.INVISIBLE);
 }
  51. 51. @Test
 public void onlyProvidesAddSessionToggleIfSessionIsNotKeynote()
 throws Exception {
 SessionDetailFragment sessionDetailFragment =
 new SessionDetailFragment();
 final SessionDetailModel sessionDetailModel =
 mock(SessionDetailModel.class);
 when(sessionDetailModel.isKeynote()).thenReturn(false);
 sessionDetailFragment.onActivityCreated(null); sessionDetailFragment.onAttach(null); sessionDetailFragment.displayData(sessionDetailModel,
 SessionDetailModel.SessionDetailQueryEnum.SESSIONS);
 final View addScheduleButton =
 sessionDetailFragment.getView()
 .findViewById(R.id.add_schedule_button); assertTrue(addScheduleButton.getVisibility() == View.INVISIBLE);
 }
  52. 52. SessionDetailFragment has low cohesion
  53. 53. TDD Live
  54. 54. –Steve Freeman and Nat Pryce, 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.”
  55. 55. Activities tend to have tight coupling and poor cohesion
  56. 56. This code is garbage
  57. 57. If Android Tests Could Talk https://twitter.com/philosohacker https://twitter.com/unikeytech

×