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.

Unit testing without Robolectric, Droidcon Berlin 2016

6,198 views

Published on

Are you bound to flaky, slow Robolectric Tests? Do you want to know a way out of it? This session shows ideas and concepts to replace Robolectric from your test code base

Published in: Engineering

Unit testing without Robolectric, Droidcon Berlin 2016

  1. 1. A life without Robolectric Droidcon 2016 @PreusslerBerlin
  2. 2. 3 years ago..... ©UniversalCityStudios
  3. 3. ©UniversalCityStudios
  4. 4. 3 years ago.....
  5. 5. Back to the future... ©UniversalCityStudios
  6. 6. ©UniversalCityStudios
  7. 7. What happened? ©UniversalCityStudios
  8. 8. What’s wrong with Robolectric? That‘s why we preferred Robolectric over Emulatur tests in the first place ©20thCenturyFox
  9. 9. What’s wrong with Robolectric? ©20thCenturyFox
  10. 10. What’s wrong with Robolectric? ©20thCenturyFox
  11. 11. What’s wrong with Robolectric? • 188 tests: • With: 4s 809ms • Without: 1s 746ms • 531 tests: • With: 7s 907ms • Without: 2s 704ms ©20thCenturyFox
  12. 12. What’s wrong with Robolectric? • Running a single test needs to be in ms • TDD bound to test performance • We run 3000+ tests ©20thCenturyFox
  13. 13. Tired of issues like java.lang.NullPointerException at org.robolectric.manifest.MetaData. init(MetaData.java:55) at org.robolectric.manifest.AndroidMa nifest.initMetaData(AndroidManifes t.java:377).... ? Don’t spent more time fixing your test setup than fixing your app Sleepy by Tomas; flickr.com/photos/tma/2438467223; CC 2.0
  14. 14. What’s wrong with Robolectric? • Developers used too much magic forgot what unit test should be • Your tests rely on correct 3rd party implementation • Tests itself became flaky
  15. 15. Android today • New developers follow MV* patterns Code is designed to be testable No need for Robolectric • Older projects were made with Robolectric • Projects not designed to be testable have often need for Robolectric
  16. 16. Welcome to Robolectric withdrawal Day care: • You have small units Start removing @RunWith(RobolectricTestrunner.class) and we treat the few remaining ambulant Long term care: • You have large units • Use lots of magic Mama will keep baby cozy and warm... by Oreste Messina; flickr.com/photos/oremessina/17338964228; CC 2.0
  17. 17. • Before: Robolectric.buildActivity( MyActivity.class).start().get() • Now: New MyActivity().onStart() Welcome to Robolectric withdrawal room to wait by Several seconds; CC 2.0; flickr.com/photos/severalseconds/16549471571; CC 2.0
  18. 18. What about views? • No one will parse your xml for you! • when(activity.findViewById(R.id.toolbar)) .thenReturn(mock(Toolbar.class); • What if class under test? Spy it: tested = spy(tested); DIY Compost Bin: Assembly by WFIU Public Radio; flickr.com/photos/wfiupublicradio/5561442658; CC 2.0
  19. 19. What about views? assertTrue( myview.getVisibility() == View.VISIBLE) What are we actually testing here? Robolectrics View implementation! What‘s the default value? Don‘t test what you don‘t own! verify(myview).setVisibility(View.VISIBLE)) Wrongside!byJérémyLelièvre;flickr.com/photos/jrmllvr/10887774436;CC2.0
  20. 20. What about Butterknife? Couldn‘t be more easy: fields are package protected @BindView(R.id.title) TextView title; Just set them: tested.title = mock(TextView.class) Butter by Joanna Bourne flickr.com/photos/66992990@N00/4819375090; CC 2.0
  21. 21. Testing Parcelation (Before) @RunWith(RobolectricTestRunner.class) public class UserTest { Parcel parcel = Parcel.obtain(); User tested = new User("123", "456"); @Test public void check_parcel_implementation() { tested.writeToParcel(parcel, 0); parcel.setDataPosition(0); User out = User.CREATOR.createFromParcel(parcel); assertEquals("123", out.getUser()); assertEquals("456", out.getPassword()); }
  22. 22. Testing Parcelation (After) Parcel parcel = mock(Parcel.class); User tested = new User("123", "456"); @Test public void should_read_from_parcel() { when(parcel.readString()).thenReturn("123", "456"); User out = User.CREATOR.createFromParcel(parcel); assertEquals("123", out.getUser()); assertEquals("456", out.getPassword()); }
  23. 23. Testing Parcelation (After) Parcel parcel = mock(Parcel.class); User tested = new User("123", "456"); @Test public void should_write_to_parcel() { tested.writeToParcel(parcel, 0); InOrder verifier = inOrder(parcel); verifier.verify(parcel).writeString("123"); verifier.verify(parcel).writeString("456"); verifier.verifyNoMoreInteractions(); }
  24. 24. Testing Parcelation (Alternative) Tip 1: move your models AutoParcel • No need for parcelation tests anymore • https://github.com/frankiesardo/auto-parcel Tip 2: move parcelation code to Parceler • https://github.com/johncarl81/parceler Parcels by delgrosso; flickr.com/photos/delgrossodotcom/2553424895; CC 2.0
  25. 25. Testing Intent building (Before) @Test public void should_create_intent() { Intent intent = MyActivity.create( mock(Context.class), deal); assertEquals( deal, intent.getParcelableExtra(“DEAL“)); } What are we actually testing here? Robolectrics Intent implementation! Dont test what you dont own!
  26. 26. Testing Intent building (After) @Test public void should_create_intent () { IntentFactory.instance = mockIntentFactory(); Intent intent = MyActivity.create( mock(Context.class), deal); verify(intent).putExtra(“DEAL", deal);} @Test public void should_create_intent () { IntentFactory.instance = mockIntentFactory(); Intent intent = MyActivity.create( mock(Context.class), deal); verify(intent).putExtra(“DEAL", deal);}
  27. 27. Testing Intent building (After) class IntentFactory { public static IntentFactory instance = new IntentFactory(); Intent create(Context ctx, Class<? extends Context> clazz){ return new Intent(ctx, clazz); } }  Call from @After public static void reset() { instance = new IntentFactory(); }
  28. 28. Testing Intent building (after) • Scared of the public instance ? • Could be private but then need setter or reflection • Whom are you afraid of? Dr. EVIL (Doctor Malito en Austin Powers) by Hersson Piratoba;
  29. 29. Testing intent building (after) Do you know the rule about encapsulation and tests? Uh, no. What rule is that? Tests trump Encapsulation. What does that mean? That means that tests win. No test can be denied access to a variable simply to maintain encapsulation. Uncle Bob https://blog.8thlight.com/uncle-bob/2015/06/30/the-little- singleton.html Why so serious? by SYD, MsSaraKelly; flickr.com/photos/mssarakelly/14403017054, CC 2.0
  30. 30. If you still dont like it.. package protected and move mocking to a class in test in same package: public class IntentFactoryMock { public static void mockFactory(Intent intent) { IntentFactory.instance = mockIntentFactory(intent); } public static void reset() { IntentFactory.instance = new IntentFactory(); } private static IntentFactory mockIntentFactory(Intent intent) { IntentFactory factory = mock(IntentFactory.class); when(factory.create()).thenReturn(intent);
  31. 31. Testing intent building (alternative) • Check out Dart and Henson: https://medium.com/groupon- eng/better-android-intents-with-dart- henson-1ca91793944b#.c7agm3ikh
  32. 32. Navigation FragmentManager fragmentManager = mock(FragmentManager.class); when(fragmentManager.beginTransaction()).thenReturn(transaction); when(transaction.replace(anyInt(), any(Fragment.class))).thenReturn(transaction); when(transaction.replace(anyInt(), any(Fragment.class), anyString())).thenReturn(transaction); when(transaction.remove(any(Fragment.class))).thenReturn(transaction); when(transaction.addToBackStack(anyString())).thenReturn(transaction); when(transaction.add(anyInt(), any(Fragment.class))).thenReturn(transaction); when(transaction.add(anyInt(), any(Fragment.class), anyString())).thenReturn(transaction); when(transaction.setCustomAnimations(anyInt(), anyInt())).thenReturn(transaction); when(transaction.setCustomAnimations(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(transaction); Mocking fragment transactions sucks Navigation by Marcus Ramberg; flickr.com/photos/marcusramberg/71281972; CC 2.0
  33. 33. Navigation by Marcus Ramberg; flickr.com/photos/marcusramberg/71281972; CC 2.0 Navigation • Wrap navigation into a component: @Inject public Interactor(FragmentTransactionsUtil util) {...} util.addAllowingStateLoss( getFragmengManager(), R.id.content, MyFragment.create());
  34. 34. Dagger, Butterknife & co. • Don‘t use Dagger in unit tests! • Call the constructor yourself Constructor injection is the only one you should use • For activites, services, fragment with @Inject fields: Set the instances directly, package protected! • KISS: Keep it simple stupid!
  35. 35. Dagger, Butterknife & co. • Never use Dagger.inject() and co directly! • Wrap it • Replace them in tests! public final class Dependencies { private static Injector instance = new DaggerInjector(); private static ViewBinder viewBinder = new ViewBinderWithButterknife(); private static ExtraBinder extraBinder = new ExtraBinderWithDart(); public static void bind(Activity activity) {...} ...
  36. 36. Butter your tests... • Introducing Diacetyl • ... is added to some foods to impart its buttery flavor (wikipedia) • It adds artifical butter flavor to your Butterknife in test environments. Pam Cooking Spray, Butter by Mike Mozart; flickr.com/photos/jeepersmedia/15203456322; CC 2.0
  37. 37. Butter your tests... class MyButterKnifeActivity { @Bind TextView textView; @Bind EditText editText; ... class MyButterKnifeActivityTest { @Test public void test() { MyButterKnifeActivtiy tested = new MyButterKnifeActivtiy(); Diacetyl.butterForTests(tested); Pam Cooking Spray, Butter by Mike Mozart; flickr.com/photos/jeepersmedia/15203456322; CC 2.0
  38. 38. Butter your tests... https://github.com/dpreussler/Diacetyl Pam Cooking Spray, Butter by Mike Mozart; flickr.com/photos/jeepersmedia/15203456322; CC 2.0
  39. 39. Version checks public boolean isRTL() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return resources.getConfiguration() .getLayoutDirection() == LAYOUT_DIRECTION_RTL; } return false; }
  40. 40. Version checks (Before) @Test @Config(reportSdk = 16) public void returns_false_on_pre_jellybeans() { Resources resources = Robolectric.buildActivity(Activity.class) .get().getResources(); RTLUtil tested = new RTLUtil(resources); assertFalse(tested.isRTL()); }
  41. 41. Version checks • Use reflection? *github.com/dpreussler/SuperReflect Reflect.on( Build.VERSION.class) .set("SDK_INT", 14); Better: wrap all the things! Reflection by Anderson Mancini; flickr.com/photos/ektogamat/3052020494; CC 2.0
  42. 42. Version checks (After) public boolean isRTL() { if (AndroidVersions.isMinJellyBeanMR1()) { return resources .getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; } return false; }
  43. 43. Version checks @Test public void returns_false_on_pre_jelly_beans() { AndroidVersions.VERSION = 16; Resources resources = mockResources(); RTLUtil tested = new RTLUtil(resources); assertFalse(tested.isRTL()); verifyZeroInteractions(resources); }
  44. 44. Version checks public final class AndroidVersions { public static int VERSION = Build.VERSION.SDK_INT; public static boolean isMinJellyBean() { return VERSION >= Build.VERSION_CODES.JELLY_BEAN; } ... }  Call from @After public static void reset() { VERSION = Build.VERSION.SDK_INT; }
  45. 45. Testing asynchronicity • Robolectric.flushBackgroundThreadScheduler() • ShadowApplication.runBackgroundTasks(); •This is wrong! • Just override runOnUiThread() instead! Rollercoaster by Eric; flickr.com/photos/eric-omba/481762682; CC 2.0
  46. 46. Testing asynchronicity (Before) MyActivity activity = Robolectric.buildActivity(MyActivity.class).get(); activity.doThingInBackground(); Robolectric.getForegroundThreadScheduler() .advanceToNextPostedRunnable(); // check what happened in runnable
  47. 47. Testing asynchronicity (After) MyActivity activity = spy(new MyActivity()); ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class); activity.doThingInBackground(); verify(activity).runOnUiThread(captor.capture()); captor.getValue().run(); ... Tip: split into 2 tests: • verify(activity).runOnUiThread(anyRunnable()) • and the captor one that does the action
  48. 48. Testing asynchronicity „Always wrap the system clock, so it can be easily substituted for testing“ (Martin Fowler) http://martinfowler.com/articles/nonDeterminism.html#Time Rollercoaster by Eric; flickr.com/photos/eric-omba/481762682; CC 2.0
  49. 49. Problem: Libraries • Support libraries • Third party libraries • Implementation not empty, Run real code on tests Delivery by Bill; flickr.com/photos/34639780@N07/16480925469; CC 2.0
  50. 50. Problem: Libraries Example: • Fragment:onStart is empty • SupportFragment:onStart actually does things Delivery by Bill; flickr.com/photos/34639780@N07/16480925469; CC 2.0
  51. 51. Problem: Libraries • Needs more mocking • Might need Reflection work • Tricky on constructors i.e. custom views extends „empty“ android views Delivery by Bill; flickr.com/photos/34639780@N07/16480925469; CC 2.0
  52. 52. Libraries (Reflection needed) CredentialsApi realApi = Auth.CredentialsApi; CredentialsApi apiMock = mock(CredentialsApi.class); SuperReflect.on(Auth.class).set("CredentialsApi", apiMock); ... tested.onConnected(mockBundle()); verify(apiMock).request( any(GoogleApiClient.class), any(CredentialRequest.class)); @After public void tearDown() { SuperReflect.on(Auth.class).set("CredentialsApi", realApi); }
  53. 53. Write it nicer • mockView() instead of mock(View.class) ... • mockEditText(„test“)that implements Editable • mockFragmentTransaction() that returns self while building • mockRecyclerView() that remembers adapter https://github.com/dpreussler/mockitoid rainbow revisited by Bill Rosgen; flickr.com/photos/wrosgen/4706169184; CC 2.0
  54. 54. Write it nicer • anyActivity() instead of any(Activity.class) • anyContext()... • anyView()... https://github.com/dpreussler/mockitoid rainbow revisited by Bill Rosgen; flickr.com/photos/wrosgen/4706169184; CC 2.0
  55. 55. Gradually stop your addiction Subscription: Unmock Decide in gradle which classes you still need from Robolectric https://github.com/bjoernQ/unmock-plugin
  56. 56. Any reasons to stick to Robolectric? “I need to test lots of UI” Test with mocks! “My tests are testing a complete flow” Your tests are too big “My tests depend on resources” Integration test? Move to Espresso?
  57. 57. Any reasons to stick to Robolectric? “I want to test my SqlLiteOpenhelper with a real database?” Wrap SqlLiteopenHelper! Assert Query Strings (Use a java version of SQLlite if you real queries) No reason! And often smell for bad code design! Fix the code not the test!
  58. 58. Groupon is Mobile One of worlds most popular apps > 50 Mio android downloads Unit testing by developers > 2000 unit test for Consumer app > 3000 unit tests for Merchant app Automation by QA engineers RoboRemote for Consumer app Appium for Merchant app
  59. 59. Danny Preussler @PreusslerBerlin ThankYoubyNateGrigg; www.flickr.com/photos/nateone/3768979925,CC2.0

×