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

Unit Testing in a Nutshell - DroidKaigi 2018

Unit Testing in a Nutshell - DroidKaigi 2018

Unit Testing in a Nutshell - DroidKaigi 2018
This is a reference material for this session
https://droidkaigi.jp/2018/timetable?session=16942
Hands-On
https://github.com/srym/DroidKaigi2018UnitTestHandOn

3cca191bf3064fd059ea2c3d6022afbd?s=128

Fumihiko Shiroyama

February 02, 2018
Tweet

Transcript

  1. Unit Testing in a Nutshell Fumihiko Shiroyama DroidKaigi 2018

  2. Who is this guy? • Senior Android Developer @nikkei •

    Executive Beer Drinker • Chief Baby Sitter at home • Unit Test Enthusiast
  3. Agenda today • Why you should care about Unit Testing

    • JUnit4 and basic assertions • Mock and spy with Mockito • Robolectric for framework's code • Unit Testing SQLite • Unit Testing asynchronous code • Best practices and tips
  4. Why you should care about Unit Testing?

  5. What if you DON'T?

  6. public class LoginActivity extends AppCompatActivity implements LoaderCallbacks<Cursor> { private static

    final int REQUEST_READ_CONTACTS = 0; private static final String[] DUMMY_CREDENTIALS = new String[]{ "foo@example.com:hello", "bar@example.com:world" }; private UserLoginTask mAuthTask = null; private AutoCompleteTextView mEmailView; private EditText mPasswordView; private View mProgressView; private View mLoginFormView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); mEmailView = (AutoCompleteTextView) findViewById(R.id.email); populateAutoComplete(); mPasswordView = (EditText) findViewById(R.id.password); mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) { attemptLogin(); return true; } return false; } }); Button mEmailSignInButton = (Button) findViewById(R.id.email_sign_in_button); mEmailSignInButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { attemptLogin(); } }); mLoginFormView = findViewById(R.id.login_form); mProgressView = findViewById(R.id.login_progress); } private void populateAutoComplete() { if (!mayRequestContacts()) { return; } getLoaderManager().initLoader(0, null, this); } private boolean mayRequestContacts() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } if (checkSelfPermission(READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { return true; } if (shouldShowRequestPermissionRationale(READ_CONTACTS)) { Snackbar.make(mEmailView, R.string.permission_rationale, Snackbar.LENGTH_INDEFINITE) .setAction(android.R.string.ok, new View.OnClickListener() { @Override @TargetApi(Build.VERSION_CODES.M) public void onClick(View v) { requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); } }); } else { requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); } return false; } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == REQUEST_READ_CONTACTS) { if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { populateAutoComplete(); } } } private void attemptLogin() { if (mAuthTask != null) { return; } mEmailView.setError(null); mPasswordView.setError(null); String email = mEmailView.getText().toString(); String password = mPasswordView.getText().toString(); boolean cancel = false; View focusView = null; if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) { mPasswordView.setError(getString(R.string.error_invalid_password)); focusView = mPasswordView; cancel = true; } if (TextUtils.isEmpty(email)) { mEmailView.setError(getString(R.string.error_field_required)); focusView = mEmailView; cancel = true; } else if (!isEmailValid(email)) { mEmailView.setError(getString(R.string.error_invalid_email)); focusView = mEmailView; cancel = true; } if (cancel) { focusView.requestFocus(); } else { showProgress(true); mAuthTask = new UserLoginTask(email, password); mAuthTask.execute((Void) null); } }
  7. public class LoginActivity extends AppCompatActivity implements LoaderCallbacks<Cursor> { private static

    final int REQUEST_READ_CONTACTS = 0; private static final String[] DUMMY_CREDENTIALS = new String[]{ "foo@example.com:hello", "bar@example.com:world" }; private UserLoginTask mAuthTask = null; private AutoCompleteTextView mEmailView; private EditText mPasswordView; private View mProgressView; private View mLoginFormView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); mEmailView = (AutoCompleteTextView) findViewById(R.id.email); populateAutoComplete(); mPasswordView = (EditText) findViewById(R.id.password); mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) { attemptLogin(); return true; } return false; } }); Button mEmailSignInButton = (Button) findViewById(R.id.email_sign_in_button); mEmailSignInButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { attemptLogin(); } }); mLoginFormView = findViewById(R.id.login_form); mProgressView = findViewById(R.id.login_progress); } private void populateAutoComplete() { if (!mayRequestContacts()) { return; } getLoaderManager().initLoader(0, null, this); } private boolean mayRequestContacts() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } if (checkSelfPermission(READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { return true; } if (shouldShowRequestPermissionRationale(READ_CONTACTS)) { Snackbar.make(mEmailView, R.string.permission_rationale, Snackbar.LENGTH_INDEFINITE) .setAction(android.R.string.ok, new View.OnClickListener() { @Override @TargetApi(Build.VERSION_CODES.M) public void onClick(View v) { requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); } }); } else { requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); } return false; } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == REQUEST_READ_CONTACTS) { if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { populateAutoComplete(); } } } private void attemptLogin() { if (mAuthTask != null) { return; } mEmailView.setError(null); mPasswordView.setError(null); String email = mEmailView.getText().toString(); String password = mPasswordView.getText().toString(); boolean cancel = false; View focusView = null; if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) { mPasswordView.setError(getString(R.string.error_invalid_password)); focusView = mPasswordView; cancel = true; } if (TextUtils.isEmpty(email)) { mEmailView.setError(getString(R.string.error_field_required)); focusView = mEmailView; cancel = true; } else if (!isEmailValid(email)) { mEmailView.setError(getString(R.string.error_invalid_email)); focusView = mEmailView; cancel = true; } if (cancel) { focusView.requestFocus(); } else { showProgress(true); mAuthTask = new UserLoginTask(email, password); mAuthTask.execute((Void) null); } } Faaaaaaaaaaaat Controller!!
  8. Fat Controller is bad because... • Slow iteration • Ambiguous

    responsibilities • Off-by-one errors • Un-reproducible bugs
  9. Unit testing lets you... • Faster execution • Clear division

    of roles • Chance to improve architecture
  10. Is unit testing just enough?

  11. No!! It just ensures it partially worked

  12. See also... • EspressoςετίʔυͷಉظॲཧΛڀΊΔ • Day1 Room5 15:40 - •

    UIςετͷ࣮ߦ࣌ؒΛ୹ॖͤ͞Δํ๏ • Day2 Room2 18:30 -
  13. anyway

  14. JUnit4 and basic assertions

  15. JUnit4 is... • A simple testing framework • Available for

    Android out-of-the-box • Still de-facto standard Moving Forward with JUnit 5 - Day2 Room5 10:30 - Check also
  16. Assertions

  17. Assertion is... • Assert something is something A rose is

    a rose is a rose is a rose... assertThat(1 + 1).isEqualTo(2); assertThat("Alice") .isNotEmpty() .isNotEqualTo("Bob");
  18. Installation dependencies { testImplementation 'junit:junit:4.12' testImplementation 'com.squareup.assertj:assertj-android:1.2.0' } assertj-android lets

    you assert Android framework's code as well
  19. Test target class InputChecker { boolean isValid(String text) { if

    (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } } VERY SIMPLE input text checker
  20. Create New Test Command + SHIFT + T

  21. Create New Test

  22. Create New Test

  23. Types of test in Android • Local Unit Tests •

    Runs on JVM • src/test • Instrumented Tests • Runs with Device/Emulators • src/androidTest
  24. Local Unit Tests: pros • Runs fast! • No device,

    emulator, android sdk
  25. Local Unit Tests: cons • No REAL Android framework's code

  26. • TRUE Android Test • Exactly the same behavior as

    production Instrumented Tests: pros
  27. Instrumented Tests: cons • Runs slow! • Need device, emulator,

    android sdk
  28. This session only cares Local Unit Tests

  29. Test class public class InputCheckerTest { @Before public void setUp()

    throws Exception { } @Test public void isValid() throws Exception { } }
  30. Test class public class InputCheckerTest { @Before public void setUp()

    throws Exception { } @Test public void isValid() throws Exception { } } Just naming convention
  31. Test class public class InputCheckerTest { @Before public void setUp()

    throws Exception { } @Test public void isValid() throws Exception { } } Called at every test case
  32. Test class public class InputCheckerTest { @Before public void setUp()

    throws Exception { } @Test public void isValid() throws Exception { } } This is a test case Write as many as you want
  33. Test class public class InputCheckerTest { @Before public void setUp()

    throws Exception { }foo @Test public void isValid() throws Exception { }foo }foo
  34. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); }foo @Test public void isValid() throws Exception { }foo }foo
  35. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { } } Initialization codes here
  36. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); }foo @Test public void isValid() throws Exception { }foo }foo
  37. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); }foo @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); Assertions.assertThat(actual).isTrue(); }foo }foo
  38. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); Assertions.assertThat(actual).isTrue(); } } Write your assertions
  39. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); Assertions.assertThat(actual).isTrue(); } } what really is "actual"
  40. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); Assertions.assertThat(actual).isTrue(); } } "expectation" what you expect it to be
  41. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); Assertions.assertThat(actual).isTrue(); } } From AssertJ Many assertion methods available
  42. Test class public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); Assertions.assertThat(actual).isTrue(); } }
  43. Run tests Run single Run all or Passed!!!!

  44. If it fails... reason why it failed failure... Fix and

    try again!!!
  45. Congrats!!!

  46. Assertions (AssertJ) • assertThat(all) • isEqualTo / isNotEqualTo • assertThat(String)

    • isEmpty / isNotEmpty • assertThat(Number) • isPositive / isNegative / isZero / isNotZero • isGreaterThan / isLessThan lots and lots and lots more
  47. • assertThat(list) • contains / containsOnly / containsExactly Assertions (AssertJ)

    lots and lots and lots more List<String> strings = Arrays.asList("foo", "bar", "baz"); assertThat(strings).contains("bar"); same collection same collection and order
  48. • http://joel-costigliola.github.io/assertj/ • https://github.com/square/assertj-android • https://qiita.com/opengl-8080/items/ b07307ab0d33422be9c5 AssertJ

  49. Exceptions

  50. Remember this... class InputChecker { boolean isValid(String text) { if

    (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } }
  51. Remember this... class InputChecker { boolean isValid(String text) { if

    (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } } What if null... Let's try it anyway
  52. Ouch!! @Test public void isValid() throws Exception { String input

    = null; boolean actual = inputChecker.isValid(input); assertThat(actual).isTrue(); }
  53. Expected Exception @Test(expected = NullPointerException.class) public void isValid() throws Exception

    { String input = null; boolean actual = inputChecker.isValid(input); assertThat(actual).isTrue(); }
  54. Expected Exception @Test(expected = NullPointerException.class) public void isValid() throws Exception

    { String input = null; boolean actual = inputChecker.isValid(input); assertThat(actual).isTrue(); }
  55. Passed!! @Test(expected = NullPointerException.class) public void isValid() throws Exception {

    String input = null; boolean actual = inputChecker.isValid(input); assertThat(actual).isTrue(); }
  56. Better yet... class InputChecker { boolean isValid(String text) { if

    (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; }foo }foo
  57. Better yet... class InputChecker { boolean isValid(@NonNull String text) {

    if (text == null) throw new IllegalArgumentException("cannot be null"); if (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; }foo }foo
  58. Better yet... class InputChecker { boolean isValid(@NonNull String text) {

    if (text == null) throw new IllegalArgumentException("cannot be null"); if (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } }
  59. Better yet... class InputChecker { boolean isValid(@NonNull String text) {

    if (text == null) throw new IllegalArgumentException("cannot be null"); if (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } } Unit testing helps you refactoring!!
  60. Way better!! @Test(expected = IllegalArgumentException.class) public void isValid() throws Exception

    { String input = null; boolean actual = inputChecker.isValid(input); assertThat(actual).isTrue(); }
  61. Mock and spy with Mockito

  62. Mock is... • A WHOLE imitation of a real object

    • Used to simulate the behavior of depending objects
  63. Spy is... • A PARTIAL imitation of a real object

    • Used to simulate the behavior of depending objects
  64. Test Doubles They are

  65. Why you need mocks?

  66. TweetRepository public class TweetRepository { private final LocalTweetDataSource localDataSource; public

    TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } }
  67. TweetRepository public class TweetRepository { private final LocalTweetDataSource localDataSource; public

    TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } } depending on local data source
  68. Data Source public class LocalTweetDataSource { public List<Tweet> getTimeline() {

    List<Tweet> tweets = new ArrayList<>(); // some local DB interaction // ..... return tweets; } }
  69. Data Source public class LocalTweetDataSource { public List<Tweet> getTimeline() {

    List<Tweet> tweets = new ArrayList<>(); // some local DB interaction // ..... return tweets; } } Because it dose so many interactions with DB You don't wanna deal with all those messes!
  70. TweetRepository public class TweetRepository { private final LocalTweetDataSource localDataSource; public

    TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } } You can replace this with simple mock
  71. Mockito

  72. Mockito is... • A simple mocking library • Most used

    one in Java and Android community
  73. Installation dependencies { testImplementation 'org.mockito:mockito-inline:2.13.0' } mockito-inline lets you mock

    final class & methods this artifact may be integrated into default in the future but you can't mock static methods & constructors
  74. Stubbing

  75. Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList(

    Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) );
  76. Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList(

    Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) );
  77. Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList(

    Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) );
  78. Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList(

    Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) ); mock instance
  79. Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn(); stub

    method
  80. Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn();

  81. Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn();

  82. Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn();

  83. Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( );zz

  84. Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList(

    Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) );zz
  85. Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList(

    Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) );zz any item you wanna return
  86. Stubbing import static org.mockito.Mockito.*; LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList(

    Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) );
  87. Stubbing private TweetRepository tweetRepository; @Before public void setUp() throws Exception

    { LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) ); tweetRepository = new TweetRepository(localDataSource); }
  88. Stubbing private TweetRepository tweetRepository; @Before public void setUp() throws Exception

    { LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ) ); tweetRepository = new TweetRepository(localDataSource); }
  89. Stubbing @Test public void getTimeline() throws Exception { List<Tweet> tweets

    = tweetRepository.getTimeline(); assertThat(tweets) .isNotEmpty() .hasSize(3) .containsExactly( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ); }
  90. Stubbing @Test public void getTimeline() throws Exception { List<Tweet> tweets

    = tweetRepository.getTimeline(); assertThat(tweets) .isNotEmpty() .hasSize(3) .containsExactly( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ); }
  91. Stubbing @Test public void getTimeline() throws Exception { List<Tweet> tweets

    = tweetRepository.getTimeline(); assertThat(tweets) .isNotEmpty() .hasSize(3) .containsExactly( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ); }
  92. Stubbing @Test public void getTimeline() throws Exception { List<Tweet> tweets

    = tweetRepository.getTimeline(); assertThat(tweets) .isNotEmpty() .hasSize(3) .containsExactly( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ); }
  93. Stubbing @Test public void getTimeline() throws Exception { List<Tweet> tweets

    = tweetRepository.getTimeline(); assertThat(tweets) .isNotEmpty() .hasSize(3) .containsExactly( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz") ); } pass!!
  94. Spying

  95. Spying is... • Stub methods can be added on a

    REAL object That means spied objects acts EXACTLY the same as real objects unless stubbed
  96. Spying List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  97. Spying List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt()); normal ArrayList
  98. Spying List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt()); wrap with spy()
  99. Spying List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt()); things you typically to with List
  100. Verify • Check the interactions with mock/spy mocks/spies will remember

    all interactions
  101. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify( );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  102. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  103. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  104. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  105. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  106. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  107. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt()); notice!
  108. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) );abc verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  109. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt());
  110. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt()); no interaction
  111. Verify List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    verify(strings, times(1)).addAll( eq(Arrays.asList("pop", "team", "epic")) ); verify(strings, never()).add(eq("pop")); verify(strings, never()).get(anyInt()); argument matcher
  112. Argument matchers • eq(T) • anyString() • anyInt() • any()

    • any(Class<T>) • nullable(Class<T>) null or T including null
  113. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when(strings.get(anyInt())).thenAnswer(invocation -> { Object[] arguments = invocation.getArguments(); int index = (int) arguments[0]; if (index < array.length) return array[index]; return "something-else"; }); String got = strings.get(123); assertThat(got).isEqualTo("something-else"); if you wanna change return val according to the arguments
  114. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when().thenAnswer(invocation -> { });
  115. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when(strings.get(anyInt())).thenAnswer(invocation -> { Object[] arguments = invocation.getArguments(); int index = (int) arguments[0]; if (index < array.length) return array[index]; return "something-else"; }); String got = strings.get(123); assertThat(got).isEqualTo("something-else");
  116. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when(strings.get(anyInt())).thenAnswer(invocation -> { Object[] arguments = invocation.getArguments(); int index = (int) arguments[0]; if (index < array.length) return array[index]; return "something-else"; }); String got = strings.get(123); assertThat(got).isEqualTo("something-else");
  117. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when(strings.get(anyInt())).thenAnswer(invocation -> { Object[] arguments = invocation.getArguments(); int index = (int) arguments[0]; if (index < array.length) return array[index]; return "something-else"; }); String got = strings.get(123); assertThat(got).isEqualTo("something-else");
  118. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when(strings.get(anyInt())).thenAnswer(invocation -> { Object[] arguments = invocation.getArguments(); int index = (int) arguments[0]; if (index < array.length) return array[index]; return "something-else"; }); String got = strings.get(123); assertThat(got).isEqualTo("something-else"); whatever logic you like
  119. Stubbing with callbacks List<String> strings = spy(new ArrayList<String>()); String[] array

    = {"pop", "team", "epic"}; strings.addAll(asList(array)); when(strings.get(anyInt())).thenAnswer(invocation -> { Object[] arguments = invocation.getArguments(); int index = (int) arguments[0]; if (index < array.length) return array[index]; return "something-else"; }); String got = strings.get(123); assertThat(got).isEqualTo("something-else"); Don't abuse this since it spoils simplicity
  120. Stubbing spy method

  121. doReturn List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    when(strings.get(3)).thenReturn("foo"); when().thenReturn() is almost always useful
  122. doReturn List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    when(strings.get(3)).thenReturn("foo"); 0 1 2 Because this is a spy, not mock, List#get() is evaluated first that causes an Exception
  123. doReturn List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    doReturn("foo").when(strings).get(3); String got = strings.get(3); Assertions.assertThat(got).isEqualTo("foo");
  124. doReturn List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    doReturn("foo").when(strings).get(3); String got = strings.get(3); Assertions.assertThat(got).isEqualTo("foo");
  125. doReturn List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    doReturn("foo").when(strings).get(3); String got = strings.get(3); Assertions.assertThat(got).isEqualTo("foo");
  126. doReturn List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    doReturn("foo").when(strings).get(3); String got = strings.get(3); Assertions.assertThat(got).isEqualTo("foo");
  127. doReturn List<String> strings = spy(new ArrayList<String>()); strings.addAll(Arrays.asList("pop", "team", "epic")); strings.forEach(System.out::println);

    doReturn("foo").when(strings).get(3); String got = strings.get(3); Assertions.assertThat(got).isEqualTo("foo"); This works well!
  128. Stubbing void method

  129. doThrow / doNothing List<String> strings = spy(new ArrayList<String>()); doThrow(new IllegalStateException()).when(strings).clear();

    strings.clear();
  130. doThrow / doNothing List<String> strings = spy(new ArrayList<String>()); doThrow(new IllegalStateException()).when(strings).clear();

    strings.clear();
  131. doThrow / doNothing List<String> strings = spy(new ArrayList<String>()); doThrow(new IllegalStateException()).when(strings).clear();

    strings.clear();
  132. doThrow / doNothing List<String> strings = spy(new ArrayList<String>()); doThrow(new IllegalStateException()).when(strings).clear();

    strings.clear();
  133. doThrow / doNothing List<String> strings = spy(new ArrayList<String>()); doThrow(new IllegalStateException()).when(strings).clear();

    strings.clear();
  134. doThrow / doNothing List<String> strings = spy(new ArrayList<String>()); doThrow(new IllegalStateException()).when(strings).clear();

    strings.clear();
  135. doThrow / doNothing List<String> strings = spy(new ArrayList<String>()); doThrow(new IllegalStateException()).when(strings).clear();

    doNothing().when(strings).clear(); strings.clear();
  136. doThrow / doNothing List<String> strings = spy(new ArrayList<String>()); doThrow(new IllegalStateException()).when(strings).clear();

    doNothing().when(strings).clear(); strings.clear();
  137. doThrow / doNothing List<String> strings = spy(new ArrayList<String>()); doThrow(new IllegalStateException()).when(strings).clear();

    doNothing().when(strings).clear(); strings.clear(); pass!!
  138. • http://site.mockito.org/ • http://static.javadoc.io/org.mockito/mockito-core/ 2.13.0/org/mockito/Mockito.html#0 Mockito REALLY HELPFUL!!

  139. Robolectric for framework's code

  140. InputChecker class InputChecker { boolean isValid(@NonNull String text) { if

    (text == null) throw new IllegalArgumentException("cannot be null"); if (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } }
  141. InputChecker class InputChecker { boolean isValid(@NonNull String text) { if

    (TextUtils.isEmpty(text)) throw new IllegalArgumentException("cannot be blank"); if (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } }
  142. InputChecker class InputChecker { boolean isValid(@NonNull String text) { if

    (TextUtils.isEmpty(text)) throw new IllegalArgumentException("cannot be blank"); if (text.length() < 4) return false; if (!text.matches("[a-zA-Z0-9]+")) return false; return true; } } android.text.TextUtils
  143. InputCheckerTest public class InputCheckerTest { private InputChecker inputChecker; @Before public

    void setUp() throws Exception { inputChecker = new InputChecker(); } @Test public void isValid() throws Exception { String input = "voodoo"; boolean actual = inputChecker.isValid(input); assertThat(actual).isTrue(); } }
  144. InputCheckerTest Basically, Android framework's code cannot be executed on JVM

    tests
  145. app/build.gradle android { testOptions { unitTests.returnDefaultValues = true } }

    notice: this setting just return the default value @Test isValid() passes successfully
  146. InputCheckerTest public class InputCheckerTest { private InputChecker inputChecker; @Before public

    void setUp() throws Exception { inputChecker = new InputChecker(); } @Test(expected = IllegalArgumentException.class) public void isValid_fails_with_blank() throws Exception { String input = ""; inputChecker.isValid(input); } } Add a test case that checks blank input
  147. InputCheckerTest public class InputCheckerTest { private InputChecker inputChecker; @Before public

    void setUp() throws Exception { inputChecker = new InputChecker(); } @Test(expected = IllegalArgumentException.class) public void isValid_fails_with_blank() throws Exception { String input = ""; inputChecker.isValid(input); } } Expected exception was not thrown! This is because of the "default value" from test options
  148. Robolectric

  149. Robolectric is... • "A unit test framework that de-fangs the

    Android SDK jar so you can test-drive the development of your Android app." • "Tests run inside the JVM on your workstation in seconds." quoted from the official site
  150. testImplementation "org.robolectric:robolectric:3.6.1" android { testOptions { unitTests { includeAndroidResources =

    true } } } Installation remove: unitTests.returnDefaultValues = true
  151. InputCheckerTest @RunWith(RobolectricTestRunner.class) public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test(expected = IllegalArgumentException.class) public void isValid_fails_with_blank() throws Exception { String input = ""; inputChecker.isValid(input); } }
  152. InputCheckerTest @RunWith(RobolectricTestRunner.class) public class InputCheckerTest { private InputChecker inputChecker; @Before

    public void setUp() throws Exception { inputChecker = new InputChecker(); } @Test(expected = IllegalArgumentException.class) public void isValid_fails_with_blank() throws Exception { String input = ""; inputChecker.isValid(input); } } Just add this!!
  153. Context in Robolectric @RunWith(RobolectricTestRunner.class) public class SQLiteTest { private SQLiteDatabase

    db; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); SQLiteOpenHelper helper = new MySQLiteOpenHelper(context); db = helper.getWritableDatabase(); } @Test public void test_db() throws Exception { // do the stuff } }
  154. Context in Robolectric @RunWith(RobolectricTestRunner.class) public class SQLiteTest { private SQLiteDatabase

    db; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); SQLiteOpenHelper helper = new MySQLiteOpenHelper(context); db = helper.getWritableDatabase(); } @Test public void test_db() throws Exception { // do the stuff } } Really handy to use!!
  155. • http://robolectric.org/ • http://robolectric.org/getting-started/ • http://robolectric.org/extending/ Robolectric One thing you

    have to notice about Robolectric is It just mimics the framework's behavior, not the real one
  156. Unit Testing SQLite

  157. • Robolectric simulates SQLite out-of-the-box!! • You can use traditional

    SQLiteOpenHelper • You can also use the latest Room Persistence Library Unit Testing SQLite
  158. Room Persistence Library dependencies { implementation "android.arch.persistence.room:runtime:1.0.0" annotationProcessor "android.arch.persistence.room:compiler:1.0.0" }

  159. Room Persistence Library @Entity public class User { @PrimaryKey private

    int uid; @ColumnInfo(name = "first_name") private String firstName; @ColumnInfo(name = "last_name") private String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } // getters & setters } Entity
  160. Room Persistence Library @Dao public interface UserDao { @Query("SELECT *

    FROM user") List<User> getAll(); @Query("SELECT * FROM user WHERE uid IN (:userIds)") List<User> loadAllByIds(int[] userIds); @Query("SELECT * FROM user WHERE first_name LIKE :first AND " + "last_name LIKE :last LIMIT 1") User findByName(String first, String last); @Insert void insertAll(User... users); @Delete void delete(User user); } Dao
  161. Room Persistence Library @Database(entities = {User.class}, version = 1) public

    abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); } Database
  162. Room Persistence Library public class UserManager { private final String

    DB_NAME = "sample_app"; private final AppDatabase db; public UserManager(@NonNull Context context) { db = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DB_NAME) .allowMainThreadQueries() .build(); } public void insertUser(@NonNull User user) { db.userDao().insertAll(user); } public List<User> getUsers() { return db.userDao().getAll(); } } Business Logic
  163. Room Persistence Library public class UserManager { private final String

    DB_NAME = "sample_app"; private final AppDatabase db; public UserManager(@NonNull Context context) { db = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DB_NAME) .allowMainThreadQueries() .build(); } public void insertUser(@NonNull User user) { db.userDao().insertAll(user); } public List<User> getUsers() { return db.userDao().getAll(); } } Business Logic just for demo's convenience
  164. Room Persistence Library public class UserManager { private final String

    DB_NAME = "sample_app"; private final AppDatabase db; public UserManager(@NonNull Context context) { db = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DB_NAME) .allowMainThreadQueries() .build(); } public void insertUser(@NonNull User user) { db.userDao().insertAll(user); } public List<User> getUsers() { return db.userDao().getAll(); } } Business Logic delegate
  165. Room Persistence Library public class UserManager { private final String

    DB_NAME = "sample_app"; private final AppDatabase db; public UserManager(@NonNull Context context) { db = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DB_NAME) .allowMainThreadQueries() .build(); } public void insertUser(@NonNull User user) { db.userDao().insertAll(user); } public List<User> getUsers() { return db.userDao().getAll(); } } Business Logic delegate
  166. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }
  167. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }
  168. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }
  169. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }
  170. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }
  171. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } }
  172. Room Persistence Library @RunWith(RobolectricTestRunner.class) public class UserManagerTest { private UserManager

    userManager; @Before public void setUp() throws Exception { Context context = RuntimeEnvironment.application.getApplicationContext(); userManager = new UserManager(context); } @Test public void insert_and_get() throws Exception { List<User> users = userManager.getUsers(); assertThat(users).isEmpty(); userManager.insertUser(new User("Fumihiko", "Shiroyama")); users = userManager.getUsers(); assertThat(users) .isNotEmpty() .hasSize(1); } } DB & table's data are reset automatically
  173. Room Persistence Library • https://developer.android.com/topic/ libraries/architecture/room.html • https://developer.android.com/topic/ libraries/architecture/adding- components.html

    • https://developer.android.com/training/ data-storage/room/index.html
  174. Unit Testing asynchronous code

  175. Testing async code • Testing asynchronous code is not always

    easy I'd introduce the very basic concept of it!
  176. Synchronous call interface Fetcher<T> { T fetch(); class StringFetcher implements

    Fetcher<String> { @Override public String fetch() { return "OK"; } } } Think of a synchronous API call over internet
  177. Simple callback Callback interfaces interface OnSuccess<T> { void onSuccess(T result);

    } interface OnFailure { void onFailure(Exception e); }
  178. Asynchronous call class AsyncFetcher<T> { private final Fetcher<T> fetcher; AsyncFetcher(Fetcher<T>

    fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess<T> onSuccess, OnFailure onFailure) { new Thread(() -> { try { Thread.sleep(1000L); onSuccess.onSuccess(fetcher.fetch()); } catch (Exception e) { onFailure.onFailure(e); } }).start(); } } Think of an asynchronous API call over internet
  179. Asynchronous call class AsyncFetcher<T> { private final Fetcher<T> fetcher; AsyncFetcher(Fetcher<T>

    fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess<T> onSuccess, OnFailure onFailure) { new Thread(() -> { try { Thread.sleep(1000L); onSuccess.onSuccess(fetcher.fetch()); } catch (Exception e) { onFailure.onFailure(e); } }).start(); } }
  180. Asynchronous call class AsyncFetcher<T> { private final Fetcher<T> fetcher; AsyncFetcher(Fetcher<T>

    fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess<T> onSuccess, OnFailure onFailure) { new Thread(() -> { try { Thread.sleep(1000L); onSuccess.onSuccess(fetcher.fetch()); } catch (Exception e) { onFailure.onFailure(e); } }).start(); } }
  181. Asynchronous call class AsyncFetcher<T> { private final Fetcher<T> fetcher; AsyncFetcher(Fetcher<T>

    fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess<T> onSuccess, OnFailure onFailure) { new Thread(() -> { try { Thread.sleep(1000L); onSuccess.onSuccess(fetcher.fetch()); } catch (Exception e) { onFailure.onFailure(e); } }).start(); } } Executed on a different thread
  182. Asynchronous call class AsyncFetcher<T> { private final Fetcher<T> fetcher; AsyncFetcher(Fetcher<T>

    fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess<T> onSuccess, OnFailure onFailure) { new Thread(() -> { try { Thread.sleep(1000L); onSuccess.onSuccess(fetcher.fetch()); } catch (Exception e) { onFailure.onFailure(e); } }).start(); } } time intensive task
  183. Asynchronous call class AsyncFetcher<T> { private final Fetcher<T> fetcher; AsyncFetcher(Fetcher<T>

    fetcher) { this.fetcher = fetcher; } void fetch(OnSuccess<T> onSuccess, OnFailure onFailure) { new Thread(() -> { try { Thread.sleep(1000L); onSuccess.onSuccess(fetcher.fetch()); } catch (Exception e) { onFailure.onFailure(e); } }).start(); } } Callbacks
  184. Asynchronous Test public class AsyncFetcherTest { private Fetcher<String> fetcher; private

    AsyncFetcher<String> asyncFetcher; @Before public void setUp() throws Exception { fetcher = spy(new Fetcher.StringFetcher()); asyncFetcher = new AsyncFetcher<>(fetcher); } @Test public void fetch_success() throws Exception { asyncFetcher.fetch(); } }
  185. Asynchronous Test public class AsyncFetcherTest { private Fetcher<String> fetcher; private

    AsyncFetcher<String> asyncFetcher; @Before public void setUp() throws Exception { fetcher = spy(new Fetcher.StringFetcher()); asyncFetcher = new AsyncFetcher<>(fetcher); } @Test public void fetch_success() throws Exception { asyncFetcher.fetch(); } }
  186. Asynchronous Test public class AsyncFetcherTest { private Fetcher<String> fetcher; private

    AsyncFetcher<String> asyncFetcher; @Before public void setUp() throws Exception { fetcher = spy(new Fetcher.StringFetcher()); asyncFetcher = new AsyncFetcher<>(fetcher); } @Test public void fetch_success() throws Exception { asyncFetcher.fetch(); } } How do I write this test?
  187. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); }, e -> log("async: ng") ); log("fetch_success: end"); }
  188. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); }, e -> log("async: ng") ); log("fetch_success: end"); } success
  189. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); }, e -> log("async: ng") ); log("fetch_success: end"); } failure
  190. Asynchronous Test @Test public void fetch_failure() throws Exception { doThrow(new

    RuntimeException("NG")).when(fetcher).fetch(); asyncFetcher.fetch( result -> log("async: ok"), e -> { assertThat(e.getMessage()).isEqualTo("NG"); log("async: ng"); } ); log("fetch_failure: end"); }
  191. Asynchronous Test @Test public void fetch_failure() throws Exception { doThrow(new

    RuntimeException("NG")).when(fetcher).fetch(); asyncFetcher.fetch( result -> log("async: ok"), e -> { assertThat(e.getMessage()).isEqualTo("NG"); log("async: ng"); } ); log("fetch_failure: end"); } stub failure
  192. Asynchronous Test Callback is NOT actually called!!! Execution seems successful...but

    wait?
  193. Asynchronous Test private CountDownLatch latch; @Before public void setUp() throws

    Exception { fetcher = spy(new Fetcher.StringFetcher()); asyncFetcher = new AsyncFetcher<>(fetcher); latch = new CountDownLatch(1); }
  194. Asynchronous Test Acts like an ON/OFF latch private CountDownLatch latch;

    @Before public void setUp() throws Exception { fetcher = spy(new Fetcher.StringFetcher()); asyncFetcher = new AsyncFetcher<>(fetcher); latch = new CountDownLatch(1); }
  195. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); latch.countDown(); }, e -> log("async: ng") ); latch.await(); log("fetch_success: end"); }
  196. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); latch.countDown(); }, e -> log("async: ng") ); latch.await(); log("fetch_success: end"); }
  197. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); latch.countDown(); }, e -> log("async: ng") ); latch.await(); log("fetch_success: end"); } Test execution awaits here
  198. Asynchronous Test @Test public void fetch_success() throws Exception { asyncFetcher.fetch(

    result -> { assertThat(result).isEqualTo("OK"); log("async: ok"); latch.countDown(); }, e -> log("async: ng") ); latch.await(); log("fetch_success: end"); } Opens the latch and test goes again
  199. Asynchronous Test @Test public void fetch_failure() throws Exception { doThrow(new

    RuntimeException("NG")).when(fetcher).fetch(); asyncFetcher.fetch( result -> log("async: ok"), e -> { assertThat(e.getMessage()).isEqualTo("NG"); log("async: ng"); latch.countDown(); } ); latch.await(); log("fetch_failure: end"); }
  200. Asynchronous Test @Test public void fetch_failure() throws Exception { doThrow(new

    RuntimeException("NG")).when(fetcher).fetch(); asyncFetcher.fetch( result -> log("async: ok"), e -> { assertThat(e.getMessage()).isEqualTo("NG"); log("async: ng"); latch.countDown(); } ); latch.await(); log("fetch_failure: end"); } The same implementation as success
  201. Asynchronous Test Callback is properly called!!! Log message seems correct!!

  202. Additional info... • MockWebServer for OkHttp • TestSubscriber for RxJava

    • https://speakerdeck.com/srym/ hayaiyasuiumai-sutatoatupudemoshi-eru- retrofit-plus-rxjava-deshun-jian- apikutukinguresipi?slide=17 Check this slide & sample project
  203. Best practices and tips

  204. Best practices and tips • Use interface at the structural

    boundary • Use DI pattern for testability • Static is EVIL • Don't be a Mad Tester
  205. Use interface at the structural boundary

  206. Repository pattern public class TweetRepository { private final LocalTweetDataSource localDataSource;

    public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } }
  207. Repository pattern public class LocalTweetDataSource { public List<Tweet> getTimeline() {

    List<Tweet> tweets = new ArrayList<>(); // some local DB interaction return tweets; } } Concrete data source
  208. Repository pattern public interface LocalTweetDataSource { List<Tweet> getTimeline(); class SQLiteTweetDataSource

    implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { List<Tweet> tweets = new ArrayList<>(); // some local DB interaction return tweets; } } class MockTweetDataSource implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { return Arrays.asList(/* some mock data */); } } }
  209. Repository pattern public interface LocalTweetDataSource { List<Tweet> getTimeline(); class SQLiteTweetDataSource

    implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { List<Tweet> tweets = new ArrayList<>(); // some local DB interaction return tweets; } } class MockTweetDataSource implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { return Arrays.asList(/* some mock data */); } } }
  210. Repository pattern public interface LocalTweetDataSource { List<Tweet> getTimeline(); class SQLiteTweetDataSource

    implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { List<Tweet> tweets = new ArrayList<>(); // some local DB interaction return tweets; } } class MockTweetDataSource implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { return Arrays.asList(/* some mock data */); } } } Concrete data source with real DB interactions
  211. Repository pattern public interface LocalTweetDataSource { List<Tweet> getTimeline(); class SQLiteTweetDataSource

    implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { List<Tweet> tweets = new ArrayList<>(); // some local DB interaction return tweets; } } class MockTweetDataSource implements LocalTweetDataSource { @Override public List<Tweet> getTimeline() { return Arrays.asList(/* some mock data */); } } } mock data source just for tests
  212. Repository pattern public class TweetRepositoryTest { private TweetRepository tweetRepository; @Before

    public void setUp() throws Exception { LocalTweetDataSource localDataSource = mock(LocalTweetDataSource.class); when(localDataSource.getTimeline()).thenReturn( Arrays.asList( Tweet.bodyOf("foo"), Tweet.bodyOf("bar"), Tweet.bodyOf("baz"))); tweetRepository = new TweetRepository(localDataSource); } }
  213. Repository pattern public class TweetRepositoryTest { private TweetRepository tweetRepository; @Before

    public void setUp() throws Exception { LocalTweetDataSource localDataSource = new LocalTweetDataSource.MockTweetDataSource(); tweetRepository = new TweetRepository(localDataSource); } } You can even get rid of Mockito dependency
  214. Use DI pattern for testability

  215. Use DI pattern public class TweetRepository { private final LocalTweetDataSource

    localDataSource; public TweetRepository() { this.localDataSource = new LocalTweetDataSource.SQLiteTweetDataSource(); } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } }
  216. Use DI pattern public class TweetRepository { private final LocalTweetDataSource

    localDataSource; public TweetRepository() { this.localDataSource = new LocalTweetDataSource.SQLiteTweetDataSource(); } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } } mocking field is painful!!
  217. Use DI pattern public class TweetRepository { private final LocalTweetDataSource

    localDataSource; public TweetRepository() { this.localDataSource = new LocalTweetDataSource.SQLiteTweetDataSource(); } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } }
  218. Use DI pattern public class TweetRepository { private final LocalTweetDataSource

    localDataSource; public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } }
  219. Use DI pattern public class TweetRepository { private final LocalTweetDataSource

    localDataSource; public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } } Putting dependency from outer
  220. Use DI pattern • This is so-called DI pattern •

    DI pattern does NOT have to use DI library • DI library (e.g. Dagger2) is useful though e.g. Factory Pattern
  221. Static is EVIL

  222. Static is EVIL because... • Static has global scope •

    Mockito cannot mock static • Static is practically unnecessary This may cause order dependent bugs
  223. Static is practically unnecessary @Singleton public class TweetRepository { private

    final LocalTweetDataSource localDataSource; @Inject public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } }
  224. Static is practically unnecessary @Singleton public class TweetRepository { private

    final LocalTweetDataSource localDataSource; @Inject public TweetRepository(LocalTweetDataSource localDataSource) { this.localDataSource = localDataSource; } public List<Tweet> getTimeline() { return localDataSource.getTimeline(); } } Use Dagger's annotations for handy singleton
  225. Don't be a Mad Tester

  226. Don't be a Mad Tester • Avoid to test EVERYTHING

    • Unit tests are NOT perfect • Gain maximum with minimum investment! Enjoy your test life!!!
  227. Thank you for listening! • All illustrations by www.irasutoya.com •

    Special Thanks to @sumio_tym, @numa08 • Get the slides: https://goo.gl/ju66No • Get the materials: https://goo.gl/R5kBeN